Sussman Lab

Neovim with lazy 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 with the excellent “Lazy” plugin manager, 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 youtube playlist going through the logic of building up a lazy config. Some of the information is out of date, but the series is nonetheless very good. The setup I’m using is loosely based on videos 1, 2, and 5.

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 Lazy

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.10.0.

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; here’s how we’ll start it out:

-- Bootstrap lazy.nvim: the next 14 lines automatically sets up the lazy plugin manager
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
    local lazyrepo = "https://github.com/folke/lazy.nvim.git"
    local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
    if vim.v.shell_error ~= 0 then
        vim.api.nvim_echo({
            { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
            { out, "WarningMsg" },
            { "\nPress any key to exit..." },
        }, true, {})
        vim.fn.getchar()
        os.exit(1)
    end
end
vim.opt.rtp:prepend(lazypath)

This clones the lazy.nvim repo and adds it to vim’s runtime path. I should mention that what follows is just a convenient, logically-separated structure for all of our options and plugins; one could also just create a massive init.lua file with everything in it (a la the kickstart repo).

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 lua/ directory – a directory which gets automatically be scanned for files – and a few new files to our configuration:

config/
|-- init.lua
|-- lua/
|   |-- options.lua
|   |-- autocmds.lua
|   |-- keymaps.lua

The contents of these files will be things like: options.lua:

vim.cmd("filetype plugin indent on")
vim.cmd("syntax enable")
vim.cmd("set number")
vim.cmd("set ruler")
vim.cmd("set relativenumber")

autocmds.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
})

keymaps.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'})
vim.keymap.set("n", "n", "nzzzv",{desc = 'find  next, center, and open any fold'})

and so on. To actually have neovim configured to use these files, we’ll add the following lines to the very bottom of our init.lua file:

-- set up leader and local leader
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
require("options")

require("keymaps")
require("autocmds")

A few things to note: first, from the examples in the options file you can see that you can wrap vim commands that might have been in a .vimrc files with a vim.cmd() in lua. There are also new ways of doing things that work, and which feel nicer to me (like the way of setting keymaps in different modes above). Finally, you can see that we’ve pre-emptively declared our leader and local-leader keys – 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

The lazy package manager makes installing plugins extremely easy. First, we need to add a line to our init.lua file. Fill in the space we left after require(options) so that part of the file becomes

require("options")
require("lazy").setup("plugins")
require("keymaps")
require("autocmds")

This tells lazy to look for a plugins/ directory, so let’s modify our configuration directory to have the following basic structure:

config/
|-- init.lua
|-- lua/
|   |-- options.lua
|   |-- autocmds.lua
|   |-- keymaps.lua
|   |-- plugins/
|   |   |-- *.lua files go here

We can now populate the plugins/ directory with any number of lua files – we could have a single one contain the information for every plugin we want to use, or a different file for each plugin, or anything in between. Personally, I like to organize the files by related functionality. For example, here is a file that uses a default configuration for two different plugins that give us access to some git tools:
gitFunctionality.lua

return {
    {
        "tpope/vim-fugitive",
        config = function()
            --vim-fugitive
            vim.keymap.set('n','<leader>gs',':Git<CR>',{noremap=true,desc ='git status'}) --git status
            vim.keymap.set('n','<leader>ga',':Git add ',{noremap=true,desc ='git add '})
            vim.keymap.set('n','<leader>gA',':Git add .<CR>',{noremap=true,desc ='git add .'})
            vim.keymap.set('n','<leader>gp',':Git push --quiet <CR>',{noremap=true,desc ='git push'})
            vim.keymap.set('n','<leader>gc',':Git commit -qam "',{noremap=true,desc ='git commit -am'})
        end
    },
    {
        "lewis6991/gitsigns.nvim",
        config = function()
            require('gitsigns').setup()
            --gitsigns
            vim.keymap.set("n", "<leader>gh", ":Gitsigns preview_hunk<CR>", {noremap=true,desc = "Gitsigns: preview [h]unk"})
            vim.keymap.set("n", "<leader>gi", ":Gitsigns preview_hunk_inline<CR>", {noremap=true,desc = "Gitsigns: preview hunk [i]nline"}) 
        end
    },
}

Notice how easy this is: we’ve just returned a table with two entries, the first part of which is literally a string that says (for instance) that https://github.com/tpope/vim-fugitive exists and has a plugin we want, and then lazy does all of the work for us. Then there is a config function that lets us configure any non-default options for the plugin (which we’ll see in more detail below), and do any other set-up work for the plugin that 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 some new files and directory structure to our config, so it now looks like:

config/
|-- init.lua
|-- lua/
|   |-- options.lua
|   |-- autocmds.lua
|   |-- keymaps.lua
|   |-- plugins/
|   |   |-- gitFunctionality.lua
|   |   |-- luasnip.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, the contents of our luasnip.lua file should be something like:
luasnip.lua

return {
	"L3MON4D3/LuaSnip",
	version = "v2.*",
    build = "make install_jsregexp",
    event = "InsertEnter",
    config = function()
        require("luasnip.loaders.from_lua").lazy_load({paths = "./lua/luasnip/"})
        local ls = require("luasnip")
        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"})
        vim.keymap.set({"i", "s"}, "<C-E>", function()
            if ls.choice_active() then
                ls.change_choice(1)
            end
        end, {silent = true, desc = "select autocomplete"})
    end
}

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 enabling some cool extra functionality (autosnippets, the ability to write snippets that modify visual selections). 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 keymaps.

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. Configuring this is substantially more involved, so rather than trying to describe exactly what I’ve set up below, I’ll just give you the important parts of the config file I’m using. There are several possible plugins you can choose from here, and I’m using nvim-cmp. See that link for documentation, tutorials, etc. Anyway, here’s a new plugin file, which is already set up to integrate with both LuaSnip and VimTex.
completions.lua

return {
    {
    "micangl/cmp-vimtex",
    ft = "tex",
    config = function()
        require('cmp_vimtex').setup({})
    end,
    },
    {
        "hrsh7th/nvim-cmp",
        event = "InsertEnter",
        dependencies = {
            "hrsh7th/cmp-buffer",--autocomplete on the buffer
            "hrsh7th/cmp-path",--autocomplete path variables
            "hrsh7th/cmp-cmdline",
            "saadparwaiz1/cmp_luasnip",--autocomplete from luasnip
            "L3MON4D3/LuaSnip",
        },

        config = function()
            local luasnip = require("luasnip")
            local cmp = require("cmp")
            cmp.setup({
                snippet = {
                    expand = function(args)
                        require('luasnip').lsp_expand(args.body)
                    end,
                },
                mapping = {
                    ['<CR>'] = cmp.mapping(function(fallback)
                        if cmp.visible() then
                            if luasnip.expandable() then
                                luasnip.expand()
                            else
                                cmp.confirm({
                                    select = true,
                                })
                            end
                        else
                            fallback()
                        end
                    end),

                    ["<Tab>"] = cmp.mapping(function(fallback)
                        if cmp.visible() then
                            if #cmp.get_entries() == 1 then
                                cmp.confirm({select =true})
                            else
                                cmp.select_next_item()
                            end
                        elseif luasnip.locally_jumpable(1) then
                            luasnip.jump(1)
                        else
                            fallback()
                        end
                    end, { "i", "s" }),

                    ["<S-Tab>"] = cmp.mapping(function(fallback)
                        if cmp.visible() then
                            cmp.select_prev_item()
                        elseif luasnip.locally_jumpable(-1) then
                            luasnip.jump(-1)
                        else
                            fallback()
                        end
                    end, { "i", "s" }),

                    ['<C-g>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }),
                    ['<C-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }),
                    ['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
                    ['<C-y>'] = cmp.config.disable,
                    ['<C-e>'] = cmp.mapping({
                        i = cmp.mapping.abort(),
                        c = cmp.mapping.close(),
                    }),
                },
                sources = cmp.config.sources({
                    { name = 'luasnip' },
                    { name = 'buffer'},
                }),
            })
            cmp.setup.cmdline({ '/', '?' }, {
                mapping = cmp.mapping.preset.cmdline(),
                sources = {
                    { name = 'buffer' }
                }
            })
            cmp.setup.cmdline(':', {
                mapping = cmp.mapping.preset.cmdline(),
                sources = cmp.config.sources({
                    { name = 'path', option = {trailing_slash = true}, }
                }, {
                        { name = 'cmdline' , option = {treat_trailing_slash =false}}
                    }),
                matching = { disallow_symbol_nonprefix_matching = false }
            })
            cmp.setup.filetype("tex", {
                sources = {
                    { name = 'vimtex'},
                    { name = 'luasnip' },
                    { name = 'buffer'},
                },
            })
        end
    }
}

It’s a lot, but mostly this is just connecting nvim-cmp (which is a completion engine) together with a bunch of completion sources (autocomplete from anything in the current buffer, or from a list of LuaSnippets, or…), and setting up some reasonable preferences. See the documentation if you want to tune things more.

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:
vimtex.lua

return {
    {
    "lervag/vimtex",
    lazy = false,
    -- tag = "v2.15", -- uncomment to pin to a specific release
    config = function()
            --global vimtex settings
            vim.g.vimtex_imaps_enabled = 0 --i.e., disable them
            --vimtex_view_settings
            vim.g.vimtex_view_method = 'general' -- change this, depending on what you want to use..sumatraPDF, or skim, or zathura, or...
            vim.g.vimtex_view_general_options = '-reuse-instance -forward-search @tex @line @pdf'
            --quickfix settings
            vim.g.vimtex_quickfix_open_on_warning = 0 --  don't open quickfix if there are only warnings
            vim.g.vimtex_quickfix_ignore_filters = {"Underfull","Overfull", "LaTeX Warning: .\\+ float specifier changed to", "Package hyperref Warning: Token not allowed in a PDF string"}
        end,
    },
}

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.

Finally, I’ve configured a few settings for how the quickfix list behaves (by default, VimTeX will show a quickfix list with various warnings and any errors that occur during compilation). Namely, most of the time I don’t care about various warnings.

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
|-- lua/
|   |-- options.lua
|   |-- autocmds.lua
|   |-- keymaps.lua
|   |-- plugins/
|   |   |-- completions.lua
|   |   |-- gitFunctionality.lua
|   |   |-- luasnip.lua
|   |   |-- vimtex.lua
|   |-- luasnip/
|   |   |-- all.lua
|   |   |-- tex.lua