Skip to main content
The vim.snippet module provides functionality for expanding and navigating LSP-style snippets. Snippets support placeholders, tabstops, variables, and transformations according to the LSP snippet specification.
Snippets follow the LSP snippet syntax specification.

Quick Start

Expand a Simple Snippet

-- Expand at cursor position
vim.snippet.expand('Hello, ${1:world}!')

-- Snippet with multiple tabstops
vim.snippet.expand('function ${1:name}(${2:args}) {\n\t$0\n}')

Jump Between Tabstops

-- Jump forward (to next tabstop)
vim.snippet.jump(1)

-- Jump backward (to previous tabstop)
vim.snippet.jump(-1)

Default Keymaps

-- Tab to jump forward
vim.keymap.set({ 'i', 's' }, '<Tab>', function()
  if vim.snippet.active({ direction = 1 }) then
    return '<Cmd>lua vim.snippet.jump(1)<CR>'
  else
    return '<Tab>'
  end
end, { expr = true })

-- Shift-Tab to jump backward
vim.keymap.set({ 'i', 's' }, '<S-Tab>', function()
  if vim.snippet.active({ direction = -1 }) then
    return '<Cmd>lua vim.snippet.jump(-1)<CR>'
  else
    return '<S-Tab>'
  end
end, { expr = true })

Core Functions

vim.snippet.expand()

Expands the given snippet text at the cursor position.
input
string
required
LSP snippet syntax string to expand
Tabstops are highlighted with SnippetTabstop and SnippetTabstopActive highlight groups.
-- Simple placeholder
vim.snippet.expand('Hello, ${1:name}!')

-- Multiple tabstops
vim.snippet.expand('for (${1:int} ${2:i} = 0; $2 < ${3:count}; $2++) {\n\t$0\n}')

-- Choice tabstop
vim.snippet.expand('${1|public,private,protected|} class ${2:Name} {}')

-- Variables
vim.snippet.expand('File: $TM_FILENAME\nLine: $TM_LINE_NUMBER')
Snippet Syntax Elements:
-- Simple tabstop
vim.snippet.expand('$1 and $2 and $0')

-- Tabstop with placeholder
vim.snippet.expand('${1:default value}')

-- Final tabstop (always $0)
vim.snippet.expand('function() {\n\t$0\n}')

vim.snippet.jump()

Jumps to the next or previous placeholder in the current snippet.
direction
1 | -1
required
Navigation direction:
  • 1: Jump to next tabstop
  • -1: Jump to previous tabstop
-- Jump to next placeholder
vim.snippet.jump(1)

-- Jump to previous placeholder
vim.snippet.jump(-1)
Behavior:
  • Automatically exits snippet when jumping to $0 (final tabstop)
  • Displays choices as completion menu if tabstop has choices
  • Syncs mirrored tabstops (same index) automatically
  • Selects placeholder text in visual mode

vim.snippet.active()

Checks if there’s an active snippet in the current buffer.
filter
table
Optional filter:
direction
1 | -1
Check if snippet can be jumped in this direction
is_active
boolean
True if snippet is active (and jumpable in direction, if specified)
-- Check if any snippet is active
if vim.snippet.active() then
  print('Snippet is active')
end

-- Check if can jump forward
if vim.snippet.active({ direction = 1 }) then
  vim.snippet.jump(1)
end

-- Check if can jump backward
if vim.snippet.active({ direction = -1 }) then
  vim.snippet.jump(-1)
end

vim.snippet.stop()

Exits the current snippet session.
vim.snippet.stop()
When snippet stops:
  • All tabstop highlights are cleared
  • Cursor movement restrictions are removed
  • Session state is cleaned up

Snippet Variables

The following variables are automatically resolved when expanding snippets:
-- Selected text (empty in insert mode)
$TM_SELECTED_TEXT
Variable with Default:
-- Use default if variable is empty/unknown
vim.snippet.expand('Author: ${TM_SELECTED_TEXT:unknown}')
Unknown Variables: Unknown variables become editable tabstops:
vim.snippet.expand('Custom: $MY_VARIABLE')
-- 'MY_VARIABLE' becomes an editable placeholder

Practical Examples

Function Template

local function_template = [[
function ${1:name}(${2:args}) {
	${3:// TODO: implementation}
	$0
}
]]

vim.snippet.expand(function_template)

If-Else Statement

local if_snippet = [[
if ${1:condition} then
	${2:-- true branch}
else
	${3:-- false branch}
end$0
]]

vim.snippet.expand(if_snippet)

Class with Constructor

local class_snippet = [[
class ${1:ClassName} {
	constructor(${2:args}) {
		${3:// initialization}
	}
	
	${4:// methods}
	$0
}
]]

vim.snippet.expand(class_snippet)

File Header

local header_snippet = [[
/**
 * File: $TM_FILENAME
 * Author: ${1:Your Name}
 * Date: ${2:2024-01-01}
 * Description: ${3:description}
 */

$0
]]

vim.snippet.expand(header_snippet)

Custom Snippet Expansion

-- Snippet database
local snippets = {
  lua = {
    fn = 'function ${1:name}(${2:args})\n\t$0\nend',
    req = 'local ${1:module} = require("${2:module}")',
  },
  python = {
    def = 'def ${1:name}(${2:args}):\n\t${0:pass}',
    cls = 'class ${1:Name}:\n\tdef __init__(self, ${2:args}):\n\t\t${0:pass}',
  },
}

-- Expand snippet by trigger
local function expand_snippet(trigger)
  local ft = vim.bo.filetype
  local snippet = snippets[ft] and snippets[ft][trigger]
  
  if snippet then
    vim.snippet.expand(snippet)
    return true
  end
  return false
end

-- Use in insert mode
vim.keymap.set('i', '<C-k>', function()
  local line = vim.api.nvim_get_current_line()
  local col = vim.api.nvim_win_get_cursor(0)[2]
  local before = line:sub(1, col)
  local trigger = before:match('[%w_]+$')
  
  if trigger and expand_snippet(trigger) then
    -- Delete trigger text
    vim.api.nvim_set_current_line(
      line:sub(1, col - #trigger) .. line:sub(col + 1)
    )
    return ''
  end
  
  return '<C-k>'
end, { expr = true })

Snippet with Transformation

-- Note: Transformations require LSP snippet grammar support
local transform_snippet = [[
const ${1:variable_name} = ${1/(\w+)/${1:/upcase}/}
]]

vim.snippet.expand(transform_snippet)
-- Typing 'hello' in first tabstop shows 'HELLO' in second

Conditional Snippet Expansion

local function smart_expand()
  -- Check if in comment
  local node = vim.treesitter.get_node()
  if node and node:type() == 'comment' then
    return false
  end
  
  -- Check if can expand
  local line = vim.api.nvim_get_current_line()
  local col = vim.api.nvim_win_get_cursor(0)[2]
  
  if line:sub(col, col):match('%w') then
    return false
  end
  
  -- Expand snippet
  vim.snippet.expand('${1:placeholder}')
  return true
end

Multi-line Snippet with Indentation

local try_catch = [[
try {
	${1:// try block}
} catch (${2:error}) {
	${3:// error handling}
} finally {
	${4:// cleanup}
}
$0
]]

vim.snippet.expand(try_catch)
-- Automatically indents based on current line

Integration with LSP

Auto-expand LSP Snippets

vim.api.nvim_create_autocmd('CompleteDone', {
  callback = function()
    local completed_item = vim.v.completed_item
    
    if completed_item and completed_item.kind == vim.lsp.protocol.CompletionItemKind.Snippet then
      local snippet = completed_item.textEdit 
        and completed_item.textEdit.newText
        or completed_item.insertText
      
      if snippet then
        -- Remove inserted text
        local line = vim.api.nvim_get_current_line()
        local col = vim.api.nvim_win_get_cursor(0)[2]
        local before = line:sub(1, col - #completed_item.word)
        local after = line:sub(col + 1)
        vim.api.nvim_set_current_line(before .. after)
        
        -- Expand snippet
        vim.snippet.expand(snippet)
      end
    end
  end,
})

Custom Snippet Source

-- Register custom LSP completion items
vim.lsp.handlers['textDocument/completion'] = function(err, result, ctx)
  if err then return end
  
  -- Add custom snippets
  local custom_snippets = {
    {
      label = 'log',
      kind = vim.lsp.protocol.CompletionItemKind.Snippet,
      insertText = 'console.log(${1:value})',
      insertTextFormat = vim.lsp.protocol.InsertTextFormat.Snippet,
    },
  }
  
  if result then
    vim.list_extend(result.items or result, custom_snippets)
  end
  
  -- Call default handler
  vim.lsp.handlers['textDocument/completion'](err, result, ctx)
end

Highlight Groups

Customize snippet appearance:
-- Inactive tabstops
vim.api.nvim_set_hl(0, 'SnippetTabstop', {
  bg = '#3b3b3b',
  fg = '#ffffff',
})

-- Active tabstop
vim.api.nvim_set_hl(0, 'SnippetTabstopActive', {
  bg = '#4a9eff',
  fg = '#ffffff',
  bold = true,
})

Advanced Techniques

Exit Snippet on Specific Keys

vim.keymap.set({ 'i', 's' }, '<Esc>', function()
  if vim.snippet.active() then
    vim.snippet.stop()
  end
  return '<Esc>'
end, { expr = true })

Snippet State Tracking

local snippet_history = {}

vim.api.nvim_create_autocmd('User', {
  pattern = 'SnippetEnter',
  callback = function()
    table.insert(snippet_history, {
      time = os.time(),
      file = vim.fn.expand('%'),
    })
  end,
})

Snippet Preview

local function preview_snippet(snippet_text)
  -- Show in floating window
  local lines = vim.split(snippet_text, '\n')
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  
  local width = 60
  local height = #lines
  local opts = {
    relative = 'cursor',
    width = width,
    height = height,
    row = 1,
    col = 0,
    style = 'minimal',
    border = 'rounded',
  }
  
  vim.api.nvim_open_win(buf, false, opts)
end

See Also

Build docs developers (and LLMs) love