Files
nvim/docs/guides/snippets.md
Morten Olsen b3b70bceeb Improved flow
2026-01-26 23:04:14 +01:00

7.8 KiB

Custom Snippets Guide

This guide explains how to create and use custom code snippets with LuaSnip.

Overview

Snippets are code templates that expand when you type a trigger and press <Tab>. Your config supports:

  • Lua snippets: Full power of Lua for dynamic snippets
  • VS Code snippets: JSON format in snippets/vscode/

Snippet Locations

~/.config/nvim/snippets/
├── all.lua           # Global snippets (all filetypes)
├── typescript.lua    # TypeScript/JavaScript
├── python.lua        # Python
├── go.lua            # Go
└── vscode/           # VS Code format snippets (optional)

Quick Start

1. Open Snippet File

-- For TypeScript snippets:
nvim ~/.config/nvim/snippets/typescript.lua

2. Add a Simple Snippet

local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node

return {
  s("cl", { t("console.log("), i(1), t(")") }),
}

3. Reload Snippets

<leader>cS    Reload all snippets

4. Use Snippet

Type cl and press <Tab> to expand.

Snippet Anatomy

Basic Structure

s(trigger, nodes, opts)
  • trigger: What you type to activate
  • nodes: The content (text, placeholders, etc.)
  • opts: Optional settings

Node Types

Node Function Purpose
t(text) Text node Static text
i(index, default) Insert node Tab stop/placeholder
c(index, choices) Choice node Multiple options
f(func, args) Function node Dynamic content
d(index, func, args) Dynamic node Complex dynamic content
rep(index) Repeat node Mirror another node

Examples by Complexity

1. Simple Text Replacement

-- Expands "todo" to "// TODO: "
s("todo", { t("// TODO: "), i(0) })

2. Multiple Tab Stops

-- Arrow function with parameters and body
s("af", {
  t("const "),
  i(1, "name"),
  t(" = ("),
  i(2),
  t(") => {"),
  t({"", "  "}),
  i(0),
  t({"", "}"}),
})

Tab order: name → parameters → body

3. Using fmt() for Cleaner Syntax

local fmt = require("luasnip.extras.fmt").fmt

s("fn", fmt([[
function {}({}) {{
  {}
}}
]], { i(1, "name"), i(2), i(0) }))

Note: {{ and }} escape braces in fmt.

4. Choice Node

-- Choose between const, let, var
s("var", {
  c(1, {
    t("const"),
    t("let"),
    t("var"),
  }),
  t(" "),
  i(2, "name"),
  t(" = "),
  i(0),
})

Press <C-n> / <C-p> to cycle choices.

5. Function Node (Dynamic)

-- Automatically generate return type
s("fn", {
  t("function "),
  i(1, "name"),
  t("("),
  i(2),
  t("): "),
  f(function(args)
    -- Generate return type based on function name
    local name = args[1][1]
    if name:match("^is") then return "boolean" end
    if name:match("^get") then return "string" end
    return "void"
  end, {1}),
  t({" {", "  "}),
  i(0),
  t({"", "}"}),
})

6. Repeat Node

-- Class with constructor that uses the class name
s("class", {
  t("class "),
  i(1, "Name"),
  t({" {", "  constructor("}),
  i(2),
  t({") {", "    "}),
  i(0),
  t({"", "  }", "", "}", ""}),
  t("export default "),
  rep(1),  -- Repeats the class name
  t(";"),
})

7. Dynamic Node

-- Generate multiple parameters dynamically
s("params", {
  t("function("),
  d(1, function()
    -- Could read from clipboard, analyze code, etc.
    return sn(nil, { i(1, "param1"), t(", "), i(2, "param2") })
  end),
  t(")"),
})

Language-Specific Examples

TypeScript

-- snippets/typescript.lua
local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local c = ls.choice_node
local fmt = require("luasnip.extras.fmt").fmt

return {
  -- Console log
  s("cl", { t("console.log("), i(1), t(")") }),
  
  -- Console log with label
  s("cll", fmt('console.log("{}: ", {})', { i(1, "label"), rep(1) })),
  
  -- Arrow function
  s("af", fmt("const {} = ({}) => {{\n  {}\n}}", { i(1, "name"), i(2), i(0) })),
  
  -- React functional component
  s("rfc", fmt([[
export function {}({}: {}) {{
  return (
    <div>
      {}
    </div>
  )
}}
]], { i(1, "Component"), i(2, "props"), i(3, "Props"), i(0) })),

  -- useState hook
  s("us", fmt("const [{}, set{}] = useState({})", {
    i(1, "state"),
    f(function(args)
      local name = args[1][1]
      return name:sub(1,1):upper() .. name:sub(2)
    end, {1}),
    i(2, "initialValue"),
  })),

  -- useEffect hook
  s("ue", fmt([[
useEffect(() => {{
  {}
}}, [{}])
]], { i(1), i(2) })),

  -- Try-catch
  s("tc", fmt([[
try {{
  {}
}} catch (error) {{
  {}
}}
]], { i(1), i(2, "console.error(error)") })),
}

Python

-- snippets/python.lua
local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local fmt = require("luasnip.extras.fmt").fmt

return {
  -- Main block
  s("main", fmt([[
if __name__ == "__main__":
    {}
]], { i(0) })),

  -- Function with docstring
  s("def", fmt([[
def {}({}):
    """{}"""
    {}
]], { i(1, "name"), i(2), i(3, "Description"), i(0) })),

  -- Class
  s("class", fmt([[
class {}:
    """{}"""
    
    def __init__(self, {}):
        {}
]], { i(1, "Name"), i(2, "Description"), i(3), i(0) })),

  -- Async function
  s("adef", fmt([[
async def {}({}):
    {}
]], { i(1, "name"), i(2), i(0) })),

  -- Try-except
  s("try", fmt([[
try:
    {}
except {} as e:
    {}
]], { i(1), i(2, "Exception"), i(3, "raise") })),

  -- Context manager
  s("with", fmt([[
with {}({}) as {}:
    {}
]], { i(1, "open"), i(2), i(3, "f"), i(0) })),
}

Go

-- snippets/go.lua
local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local c = ls.choice_node
local fmt = require("luasnip.extras.fmt").fmt

return {
  -- Error handling
  s("iferr", fmt([[
if err != nil {{
	return {}
}}
]], { c(1, { t("err"), i(nil, "nil, err") }) })),

  -- Function
  s("fn", fmt([[
func {}({}) {} {{
	{}
}}
]], { i(1, "name"), i(2), i(3, "error"), i(0) })),

  -- Method
  s("meth", fmt([[
func ({} *{}) {}({}) {} {{
	{}
}}
]], { i(1, "r"), i(2, "Receiver"), i(3, "Method"), i(4), i(5, "error"), i(0) })),

  -- Struct
  s("st", fmt([[
type {} struct {{
	{}
}}
]], { i(1, "Name"), i(0) })),

  -- Interface
  s("iface", fmt([[
type {} interface {{
	{}
}}
]], { i(1, "Name"), i(0) })),

  -- Test function
  s("test", fmt([[
func Test{}(t *testing.T) {{
	{}
}}
]], { i(1, "Name"), i(0) })),

  -- Table-driven test
  s("ttest", fmt([[
func Test{}(t *testing.T) {{
	tests := []struct {{
		name string
		{}
	}}{{
		{{}},
	}}
	
	for _, tt := range tests {{
		t.Run(tt.name, func(t *testing.T) {{
			{}
		}})
	}}
}}
]], { i(1, "Name"), i(2, "// fields"), i(0) })),
}

VS Code Format Snippets

You can also use JSON snippets in snippets/vscode/:

// snippets/vscode/typescript.json
{
  "Console Log": {
    "prefix": "cl",
    "body": ["console.log($1)"],
    "description": "Console log"
  },
  "Arrow Function": {
    "prefix": "af",
    "body": [
      "const ${1:name} = (${2:params}) => {",
      "  $0",
      "}"
    ],
    "description": "Arrow function"
  }
}

Tips

Trigger Naming

Use short, memorable triggers:

  • cl → console.log
  • fn → function
  • iferr → if err != nil

Tab Stop Order

  • i(1) → First tab stop
  • i(2) → Second tab stop
  • i(0) → Final cursor position (always last)

Default Values

i(1, "default")  -- Shows "default", selected for replacement

Testing Snippets

  1. Edit snippet file
  2. <leader>cS - Reload
  3. Open file of that type
  4. Type trigger + <Tab>

Debugging Snippets

If snippet doesn't work:

  1. Check Lua syntax (:luafile %)
  2. Verify filetype matches
  3. Check trigger isn't conflicting

Reloading

<leader>cS    Reload all custom snippets

Changes take effect immediately after reload.