Making a convenient patch to neovim

2026-04-11

Neovim allows users to use LSP-style snippets natively. By default, there are only a few variables that can be replaced with special text.

TM_SELECTED_TEXTThe currently selected text or the empty string
TM_CURRENT_LINEThe contents of the current line
TM_CURRENT_WORDThe contents of the word under cursor or the empty string
TM_LINE_INDEXThe zero-index based line number
TM_LINE_NUMBERThe one-index based line number
TM_FILENAMEThe filename of the current document
TM_FILENAME_BASEThe filename of the current document without its extensions
TM_DIRECTORYThe directory of the current document
TM_FILEPATHThe full file path of the current document

These are great, but I wanted to make a snippet that included the current date and there’s no way to do that without adding a plugin such as vim-vsnip or luasnip. I build my Neovim from source so looking at the innards is convenient, so I figured I’d look a how these variables work and it’s pretty straight forward.

local function expand_or_default(str)
  local expansion = vim.fn.expand(str) --[[@as string]]
  return expansion == '' and default or expansion
end

if var == 'TM_SELECTED_TEXT' then
  -- Snippets are expanded in insert mode only, so there's no selection.
  return default
elseif var == 'TM_CURRENT_LINE' then
  return vim.api.nvim_get_current_line()
elseif var == 'TM_CURRENT_WORD' then
  return expand_or_default('<cword>')
elseif var == 'TM_LINE_INDEX' then
  return tostring(vim.fn.line('.') - 1)
elseif var == 'TM_LINE_NUMBER' then
  return tostring(vim.fn.line('.'))
elseif var == 'TM_FILENAME' then
  return expand_or_default('%:t')
elseif var == 'TM_FILENAME_BASE' then
  return expand_or_default('%:t:r')
elseif var == 'TM_DIRECTORY' then
  return expand_or_default('%:p:h:t')
elseif var == 'TM_FILEPATH' then
  return expand_or_default('%:p')
end

After looking at this, I just needed to add a way to declare a function that returned a string of what I wanted it to be replaced with. So I added this:

-- runtime/lua/vim/snippet.lua resolve_variable function
if vim.g.snippet_vars ~= nil and vim.g.snippet_vars[var] ~= nil then
  return vim.g.snippet_vars[var]()
end

and in my init.lua this

vim.g.snippet_vars = {
  TM_DATE = function ()
    return os.date('%Y-%m-%d')
  end
}

The patch checks a global variable called vim.g.snippet_vars that contains a table of key names and functions that return a string as the value. So for my TM_DATE snippet (I figured I’d keep the naming convention from the standard variables), I just have a function that returns the current date in ISO format like so:

vim.snippet.expand("$TM_DATE") -- expands to 2026-04-11

To make sure that my patch would continue working in the future, I also added a spec that tested out this functionality. And just like that, I now have the ability to declare custom snippet variables natively! “But what if you don’t have your custom build installed?” In that case the default behavior of snippet variables will work which will treat $TM_DATE as a placeholder.

P.S. I don’t plan on making this a pull request to upstream because the core maintainers mentioned that they want the snippet functionality to adhere strictly to the LSP spec.