The sections below discuss configuring neovim, and walk through how to set up some of my favorite plugins. If you prefer to cut to the chase, feel free to take a look at the associated dotfiles with my current configuration.
Neovim with vim.pack plugin management
Neovim is a fork of vim that extends / refactors vim, keeping all of the editing experiences you expect but with many nice additional features. It has a great community, a fantastic plugin ecosystem for enhancing your vim experience, and doesn’t require you to learn vimscript. On this page we’ll walk through a basic setup of neovim, configuring it using neovim’s native vim.pack functionality (introduced in version 0.12), and installing a few of the most useful plugins for writing LaTeX documents in neovim. There are a lot of things I’m not going to touch on here – for instance, how neovim interfaces very nicely with LSPs when you are programming – and instead will focus on the LaTeX side of things. I came from a background of mostly vanilla vim, and in learning all of this I found the following references quite helpful:
- The kickstart starter git repo.
- A related video from TJ DeVries.
- A guide to
vim.packfrom Evgeni Chasnovski.
Finally, just as a note: I’ll be writing this assuming that you are already largely familiar with vim. Here is a collection of excellent resources I can recommend if you’re interested in starting to learn!
Installing neovim and setting up our directory
Installing neovim is straightforward on all platforms: on Windows just do $ winget install Neovim.Neovim; on MacOS you can $ brew install neovim && brew link neovim; on Ubuntu it’s $ sudo apt install neovim; and so on. (If you want to use an iPad, this repo has some info). Depending on your package manager you may or may not be getting the most recent neovim release; below I’ll assume that you at least have version 0.12.0 so that we can take advantage of the native vim.pack package manager.
To start our configuration we’re going to want to put a file named init.lua in the right place. Open up neovim and type :echo stdpath('config') – the result that shows up is where our config directory will be (on Windows this will be something like ~\AppData\Local\nvim; on MacOS and Ubuntu it will be something like ~/.config/nvim/). Throughout this document I’ll show files in our configuration directory like this:
config/
|-- init.lua
We have to actually create that init.lua file, but because we are going to use neovim’s built-in plugin/ directory to load our configuration sequentially, our init.lua can be extremely minimal. At the moment mine is just
vim.loader.enable()
This simply enables neovim’s experimental bytecode loader for faster startup times. (I also have a brief description of some of the parts of my config that are more esoteric / not relevant for this guide.)
Setting up vim options and autocommands, and keymaps
Now, if you are migrating from vim (like I was) you probably had a .vimrc file with your favorite options, autocommands, and keymaps. To do this in neovim we’ll add a plugin/ directory. This directory gets automatically scanned and executed by neovim on startup, which will process the files in alphabetical order. Thus, we’ll prefix our filenames with numbers to control the order in which they load:
config/
|-- init.lua
|-- plugin/
| |-- 00options.lua
| |-- 02autocmds.lua
| |-- 03baseKeymaps.lua
One does not need to make these separate files – I just find it convenient to organize a handful of smaller files rather than having one monolithic one. Just personal preference. The contents of these files will be things like: 00options.lua:
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
vim.cmd("filetype plugin indent on")
vim.cmd("syntax enable")
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.ruler = true
vim.undofile = true
-- ..
02autocmds.lua:
local autogroup = vim.api.nvim_create_augroup
local sussmanGroup = autogroup('DMS',{})
local autocmd=vim.api.nvim_create_autocmd
-- resize splits if the window itself is resized
autocmd('VimResized',{
group=sussmanGroup,
callback = function()
local currentTab=vim.fn.tabpagenr()
vim.cmd("tabdo wincmd =")
vim.cmd("tabnext " .. currentTab)
end
})
03baseKeymaps.lua:
vim.keymap.set("v", "J", ":m '>+1<CR>gv=gv",{desc ='switch current line with the one below'})
vim.keymap.set("v", "K", ":m '<-2<CR>gv=gv",{desc = 'switch current line with the one above'})
Because neovim automatically executes every .lua file inside the plugin/ directory, we don’t even need to require() these files in our init.lua. They just work! Notice also that we’ve pre-emptively declared our leader and local-leader keys in the 00options.lua file – we want to do this before we load any plugins so that the plugins define their own keymaps consistent with what we want.
Installing plugins
With Neovim 0.12, the native package manager makes installing plugins extremely easy. We can just call vim.pack.add() with the URL of the git repository. We can continue populating our plugin/ directory to keep our plugin configurations logically separated. For example, here is a file that sets up a plugin for editing the filesystem in a buffer: 20oil.lua
vim.pack.add({ "https://github.com/stevearc/oil.nvim" })
require("oil").setup({})
vim.keymap.set("n", "-", "<CMD>Oil<CR>", { desc = "Open parent directory" })
Notice how easy this is: we literally pass a string with the plugin’s URL to vim.pack.add, and neovim downloads and adds it to the runtime path for us. Then we just follow it up with whatever configuration or keymaps we want.
There are many plugins that have been written for neovim – just look at this list organized by theme! – and I have no intention of really even talking about the ones that I use a lot (like telescope). Here I want to focus on what I think are the indispensable set of plugins I work with for LaTeX writing.
Luasnip
Having access to a powerful snippet engine that lets us easily create and customize our own snippets makes writing LaTeX documents so much more pleasant. At their most trivial level, snippets allow you to type ;a and have that automatically expand to the string \alpha, or have fig optionally expand into favorite sequence of LaTeX commands for defining a figure environment, or… I’ll go into detail about how you can actually use snippets here); in this document I’ll just discuss configuring the snippet engine plugin, LuaSnip.
To start out, we’ll add a new lua/ directory for our snippet files (which we don’t want automatically executed on startup like the files in plugin/):
config/
|-- init.lua
|-- plugin/
| |-- 00options.lua
| |-- 02autocmds.lua
| |-- 03baseKeymaps.lua
| |-- 20oil.lua
| |-- 30luasnip.lua
|-- lua/
| |-- luasnip/
| | |-- all.lua
| | |-- tex.lua
The files in the luasnip/ directory are where we’ll actually write our snippets, and the file names here matter: all.lua snippets will be available when neovim is editing any file, tex.lua snippets will be available when neovim is editing text files, md.lua snippets would be available for markdown files, etc etc. We’ll get into the contents of those files and some of the cool things we can do on the luasnippets page.
Now, to install the LuaSnip plugin and ensure its C-extension for regular expressions is built, the contents of our 30luasnip.lua file should be something like: 30luasnip.lua
vim.pack.add({ "https://github.com/L3MON4D3/LuaSnip" })
-- auto-build jsregexp C extension if missing
local function ensure_jsregexp_built()
local luasnip_lua_path = vim.api.nvim_get_runtime_file("lua/luasnip/init.lua", false)[1]
if not luasnip_lua_path then return end
local luasnip_root = vim.fn.fnamemodify(luasnip_lua_path, ":h:h:h")
local artifact = luasnip_root .. "/deps/jsregexp/jsregexp.so"
if vim.fn.has("win32") == 1 then
artifact = luasnip_root .. "/deps/jsregexp/jsregexp.dll"
end
if not vim.uv.fs_stat(artifact) then
vim.notify("Building LuaSnip jsregexp in the background...", vim.log.levels.INFO)
vim.system({"make", "install_jsregexp"}, { cwd = luasnip_root }, function(out)
vim.schedule(function()
if out.code == 0 then
vim.notify("LuaSnip jsregexp built successfully!", vim.log.levels.INFO)
else
vim.notify("Failed to build LuaSnip jsregexp:\n" .. (out.stderr or out.stdout or ""), vim.log.levels.ERROR)
end
end)
end)
end
end
ensure_jsregexp_built()
local snippet_path = vim.fn.stdpath("config") .. "/lua/luasnip/"
require("luasnip.loaders.from_lua").lazy_load({ paths = { snippet_path } })
local ls = require("luasnip")
local types = require("luasnip.util.types")
ls.setup({
update_events = { "TextChanged", "TextChangedI" },
enable_autosnippets = true,
store_selection_keys = "<Tab>",
})
vim.keymap.set({ "i" }, "<C-k>", function() ls.expand() end, { silent = true, desc = "expand autocomplete" })
vim.keymap.set({ "i", "s" }, "<C-j>", function() ls.jump(1) end, { silent = true, desc = "next autocomplete" })
vim.keymap.set({ "i", "s" }, "<C-L>", function() ls.jump(-1) end, { silent = true, desc = "previous autocomplete" })
There are many more options that we could configure if we wanted (see the documentation on the git repo), but this is a good starting place. We’re telling LuaSnip where to look for our snippets, we’re making sure the jsregexp dependency builds itself in the background, and we’re enabling some cool extra functionality (autosnippets, visual text indicators for active nodes). Note, importantly, that this is where we define keymaps for expanding a snippet, and also for moving through more complex snippets. This will make more sense when we dive into snippets here, but it’s worth highlighting now. Note that in the autocompletion section below there are also some LuaSnip-related configurations.
Autocompletion in neovim
Autocompletion is a feature of most modern editors, and while vim and neovim have some built-in functionality for this, adding a completion plugin can add some nice quality-of-life features. I am currently using blink.cmp, which is an incredibly fast completion engine that integrates beautifully with LuaSnip and our LaTeX workflow.
Here is a configuration file, 50autocomplete.lua, that sets up blink.cmp along with a compatibility layer so that it can talk to VimTeX’s completion sources:
50autocomplete.lua
vim.pack.add({ "https://github.com/micangl/cmp-vimtex" })
vim.pack.add({ "https://github.com/saghen/blink.lib",
"https://github.com/saghen/blink.compat",
"https://github.com/saghen/blink.cmp" })
local cmp = require('blink.cmp')
require("blink.compat").setup({})
cmp.setup({
keymap = {
preset = 'default',
['<Tab>'] = { 'snippet_forward', 'fallback' },
['<S-Tab>'] = { 'snippet_backward', 'fallback' },
},
snippets = {
preset = 'luasnip',
expand = function(snippet) require('luasnip').lsp_expand(snippet) end,
active = function(filter)
if filter and filter.direction then
return require('luasnip').locally_jumpable(filter.direction)
end
return require('luasnip').in_snippet()
end,
jump = function(direction) require('luasnip').jump(direction) end,
},
sources = {
default = { 'lsp', 'path', 'cmdline', 'snippets', 'buffer', 'vimtex' },
providers = {
snippets = { score_offset = 10 },
vimtex = {
name = 'vimtex',
module = 'blink.compat.source',
score_offset = 15,
},
},
},
signature = { enabled = true },
completion = {
documentation = {
auto_show = true,
window = { border = "single" },
},
},
})
vim.schedule(function()
cmp.build()
end)
Kind of a lot of configuration options, but these completion engines are worth it! It ties everything together: paths, command lines, snippets, buffers, and even VimTeX citation and command completion.
VimTeX
Having access to a completion engine and snippets makes writing LaTeX documents in vim fun, but the fantastic VimTex plugin (which works for both neovim and vim) is more than half of the reason to think that its reasonable to move your Tex workflow to vim from TexShop (or TeXnicCenter, or BaKoMa, or overleaf, or wherever). It defines new vim motions and commands, lets you compile documents without leaving vim, allows for easy synchronization with pdf viewers while you’re typing, and so on. The documentation on the VimTeX git repo is excellent, so you should probably just check that out.
I’ll discuss some of the features that I like – along with a few demonstrations – of VimTeX here. For now, let’s just add a file to our plugins and get things configured. Note that for VimTeX, it’s very important to set any global variables (vim.g.*) before the plugin is loaded!
80latex.lua
-- We need to configure vimtex globals BEFORE loading the plugin
vim.g.vimtex_imaps_enabled = 0 -- disable imaps (luasnip instead)
-- vimtex view settings... you'll need to configure based on your pdf viewer of choice
vim.g.vimtex_view_method = 'general'
vim.g.vimtex_view_general_options = '-reuse-instance -forward-search @tex @line @pdf'
vim.pack.add({ "https://github.com/lervag/vimtex" })
This has just a tiny subset of the many options that can be set in VimTeX, and there’s a substantial amount of tinkering that you can (and should!) do in configuring this plugin. To briefly describe what we’ve set in the configuration above, the first thing I’m doing is turning off the insert mode mappings that VimTeX provides. These mappings are a little bit like some of the snippets we’ll set up using LuaSnip, but (1) they are less flexible / configurable, and (b) I think it’s better to take the time and create snippets that make sense and are helpful to you rather than trying to memorize a set that made sense to someone else.
We then set two options for how VimTeX interacts with an external PDF viewer. VimTeX supports a relatively wide set of programs to open the pdfs that it will compile from your tex files – this includes both forward jumping from your tex source to that spot of the pdf and inverse searching from the pdf to that part of your tex source. Each different pdf viewer requires different settings to configure these options; see the VimTex documentation for all of the details.
There are many other options you can set here – again, see the documentation – ranging from what LaTeX compiler you want to use, to whether you want to enable various flavors of vim folding, to concealing mathematics and citations and replacing them with (more? less?) readable forms when your cursor is on a different line. I’ll show some, but not all, of the features that can be configured along with other demonstrations of using VimTeX here. In the meantime, we’ve ended up at a place where our neovim configuration directory looks like this:
config/
|-- init.lua
|-- plugin/
| |-- 00options.lua
| |-- 02autocmds.lua
| |-- 03baseKeymaps.lua
| |-- 20oil.lua
| |-- 30luasnip.lua
| |-- 50autocomplete.lua
| |-- 80latex.lua
|-- lua/
| |-- luasnip/
| | |-- all.lua
| | |-- tex.lua
