Template files for nvim

  Wednesday, November 20, 2024 » Neovim

Over the years I’ve picked up various tweaks for my nvim configuration. One of them is template file support. This article is a short introduction to what they are, how do they work and how I recently extended them to support snippet expansion.

BufNewFile

nvim supports a BufNewFile autocommand that’s triggered when starting to edit a file that doesn’t exist.

I use this to implement template files. If I run :e /tmp/newfile.sh within nvim the BufNewFile autocommand triggers and looks for a template in ~/.config/nvim/templates/<ext>.tpl. If it exists it reads the file contents into the buffer. The <ext>.tpl is derived from the filename, in this case it would be sh.tpl.

sh.tpl looks like this:

#!/usr/bin/env bash
set -Eeuo pipefail

For the longest time I had set this up in a vimscript autocmd definition like this:

autocmd BufNewFile * silent! 0r $HOME/.config/nvim/templates/%:e.tpl

Lua and exact match

At some point I wanted to have template files which are exact matches, mostly for pom.xml files for Maven. Because of that I turned the above vimscript one-liner into Lua code that also checks if there is an exact match. pom.xml.tpl in this case:

vim.api_nvim_create_autocmd("BufNewFile", {
  group = vim.api.nvim_create_augroup("templates", { clear = true }),
  desc = "Load template file",
  callback = function(args)
    local home = os.getenv("HOME")
    -- fnamemodify with `:t` gets the tail of the file path: the actual filename
    -- See :help fnamemodify
    local fname = vim.fn.fnamemodify(args.file, ":t")
    local tmpl = home .. "/.config/nvim/templates/" .. fname ..".tpl"
    -- fs_stat is used to check if the file exists
    if vim.uv.fs_stat(tmpl) then
      -- See :help :read
      -- 0 is the range:
      -- This reads as: "Insert the file <tmpl> below the specified line (0)"
      vim.cmd("0r " .. tmpl)
    else
        -- fnamemodify with `:e` gets the filename extension
      local ext = vim.fn.fnamemodify(args.file, ":e")
      tmpl = home .. "/.config/nvim/templates/" .. ext ..".tpl"
      if vim.uv.fs_stat(tmpl) then
        vim.cmd("0r " .. tmpl)
      end
    end
  end
})

The template itself looked like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <properties>
  </properties>

  <build>
  </build>

  <dependencies>
  </dependencies>
</project>

Snippets

Some of my templates ended up having a couple of TODO markers, which I’d manually need to update. With the addition of vim.snippet.expand in nvim I saw the opportunity to do something about that and extended the BufNewFile autocommand to expand the template’s content as snippet:

vim.api.nvim_create_autocmd("BufNewFile", {
  group = vim.api.nvim_create_augroup("templates", { clear = true }),
  desc = "Load template file",
  callback = function(args)
    local home = os.getenv("HOME")
    local fname = vim.fn.fnamemodify(args.file, ":t")
    local ext = vim.fn.fnamemodify(args.file, ":e")
    local candidates = { fname, ext }
    local uv = vim.uv
    for _, candidate in ipairs(candidates) do
      local tmpl = table.concat({ home, "/.config/nvim/templates/", candidate, ".tpl" })
      if uv.fs_stat(tmpl) then
        vim.cmd("0r " .. tmpl)
        return
      end
    end
    for _, candidate in ipairs(candidates) do
      local tmpl = table.concat({ home, "/.config/nvim/templates/", candidate, ".stpl" })
      local f = io.open(tmpl, "r")
      if f then
        local content = f:read("*a")
        vim.snippet.expand(content)
        return
      end
    end
  end
})

The script now tries:

  • <filename>.tpl
  • <filename_extension>.tpl
  • <filename>.stpl
  • <filename_extension>.stpl

Where .tpl files are read as is into the buffer, and .stpl files treated as snippet which expand via vim.snippet.expand.

My new pom.xml.stpl looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>${1:group}</groupId>
  <artifactId>${2:artifact}</artifactId>
  <version>${3:0.1.0}</version>
  <packaging>${4:jar}</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>${5:21}</maven.compiler.source>
    <maven.compiler.target>${5:21}</maven.compiler.target>
  </properties>

  <build>
  </build>

  <dependencies>
  </dependencies>
</project>

With this in place opening a new pom.xml will start in snippet mode with group, artifact, version, jar, and the JDK verison highlighted and ready to jump through each via <tab> to fill in the correct values.