--- Title: Neovim as a LaTex Development Environment Date: 2023-10-14 12:00:00+0200 Date: 2023-10-14 17:00:00+0200 Lang: en Author: Fabrice Category: software Tags: vim, neovim, latex, zathura Slug: nvim-latex table-of-contents: true Header_Cover: ../images/covers/fern-forest.jpg Summary: How to turn Neovim into a full-fledged latex development environment --- # Introduction [LaTeX](https://en.wikipedia.org/wiki/LaTeX) is a typesetting software for producing typographically sound printable documents that is mostly used by the scientific community (but [not only](https://www.ctan.org/pkg/latex-sciences-humaines)) as it allows writing mathematics formulae in a somewhat *not-that-much painful* way, is shipped with [bibliography engines](https://www.ctan.org/pkg/biblatex), enables easy cross-referencing and automatically generates table of contents. It is based on a markup language that allows writers to focus on the content of the document and leaves the typesetting to the software (at least most of the time). It moreover enjoys [many](https://ctan.org/) libraries that span from enabling [new features](https://ctan.org/pkg/algorithm2e) to [simpler version](https://www.ctan.org/pkg/a4wide) of more [complete tools](https://ctan.org/pkg/geometry). In this blog post we will see how to setup [Neovim](https://neovim.io/) to manipulate LaTeX document while enabling modern features such as [language server protocol](https://en.wikipedia.org/wiki/Language_Server_Protocol) and what you want from any LaTeX IDEs: forward and backward searches (respectively going from the source code to the resulting document and vice-versa, thanks to [synctex](https://tug.org/tugboat/tb29-3/tb93laurens.pdf)). As a PDF reader, we will use [zathura](https://pwmt.org/projects/zathura/) to show how to setup backward search (search from the document toward the source). It is a highly configurable, lightweight document viewer which natively enjoys vim-like shortcuts. # Ingredients Before starting we will need several components to achieve this lofty goal of painlessly writing LaTeX documents with the best text editor. Let us start by listing them; we will shortly see the installation and configuration procedure: * A configurable text editor to be able to write the document: [Neovim](https://neovim.io). For that we will also need some plugins to unleash its full capability: * [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig): a plugin to facilitate the configuration [LSP](https://en.wikipedia.org/wiki/Language_Server_Protocol) for `Neovim`. * [nvim-cmp](https://github.com/hrsh7th/nvim-cmp): a completion engine for `Neovim`. * [vimtex](https://github.com/lervag/vimtex): a language specific plugin for LaTeX files that supports many features such as accurate syntactic coloration, support of multi-files, add LaTeX-specific [text objects](https://vimhelp.org/motion.txt.html#text-objects), improved foldings and so on. * [texlab](https://github.com/latex-lsp/texlab): to enable LSP features, you also a LSP server for vim to communicate with, which is exactly what `texlab` is. * [zathura](https://pwmt.org/projects/zathura/): finally a PDF viewer, we will use `zathura` here, but `vimtex` supports many others with predefined setups. However you will have to look for the specific documentation of your pdf reader to enable reverse search if it is possible. # Setting Neovim up Now that we have prepared everything, we need to setup `Neovim`. We will assume a blank configuration and start from scratch. I got inspired by a [blogpost about snippets in Neovim](https://pcoves.gitlab.io/en/blog/nvim-snippets/#installation) and used `NVIM_APPNAME` environment variables for testing this configuration. Please let me know if anything is not working as intended. ## Being Lazy Anyhow, we first need to install the different plugins that we need. For this purpose, I used the [lazy](https://github.com/folke/lazy.nvim) plugin manager, but you can use whichever you see fit for the task. ```lua -- Lazy Package Manager local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" if not vim.loop.fs_stat(lazypath) then vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", -- latest stable release lazypath, }) end vim.opt.rtp:prepend(lazypath) -- Packages require("lazy").setup({ "lervag/vimtex", "neovim/nvim-lspconfig", "hrsh7th/cmp-nvim-lsp", "hrsh7th/nvim-cmp", }) ``` In the code block above —in `$NVIM_CONFIG/init.lua`— the first part is to bootstrap lazy (so it can install itself if not already there) and the last block describe the installation of the following plugins : `vimtex`, `nvim-lspconfig`, `nvim-cmp` and finally `cmp-nvim-lsp` to glue the completion engine and `lspconfig`. Now it is all good and done, but nothing is configured yet, and if you open a LaTeX file in this state, you will only enjoy the benefits of an unconfigured `vimtex`, which is already nice as is it, but not enough to achieve our goal. And it's a bit sad to have installed three other plugins for nothing. # vimtex It will be a bit anti-climatic after the previous teasing, but we will use `vimtex` as vanilla as possible… We still need to tell it to use `zathura` as a pdf viewer: ```lua vim.g.vimtex_view_method = "zathura" ``` This will allow `vimtex` to automatically open `zathura` upon compilation, which is bound to `ll` by default. Meaning that we have to define [``](https://neovim.io/doc/user/map.html#%3CLocalLeader%3E), which I usually set to be a comma: “`,`”: ```lua vim.g.maplocalleader = "," ``` Now, you can use `,lv` to view the current line in `zathura`, and `,ll` to compile your document. Yay! More can be then done, such as using vimtex folds, which are disabled by default (contrary to what [vim-latex](https://github.com/vim-latex/vim-latex) was doing, which is the former plugin I used): ```lua -- From: https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L4671-L4713 vim.o.foldmethod = "expr" vim.o.foldexpr="vimtex#fold#level(v:lnum)" vim.o.foldtext="vimtex#fold#text()" -- I like to see at least the content of the sections upon opening vim.o.foldlevel=2 ``` Now the sky is your limit, but to start with, here follows a quick list of what is possible now: - Compile the document: `,ll` - This also automatically generates a [quickfix buffer](https://vimhelp.org/quickfix.txt.html) which is quite complete… even a tad bit too much sometimes. I used it as is to hunt for over/underfull hboxes, but you can filter them out by setting the [`vim.g.vimtex_quickfix_ignore_filters`](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L2365-L2378) variable. - View the current location in the document: `,lv`. - Show table of content navigation: `,lt`. * Using latex-specific text objects such as `$` for math or `e` for environment (defined by `\begin{…}` and `\end{…}`). - Insert command/environment : `/` (in normal and visual mode; these are not very accessible, but can be remapped). - Support for [TeX directives](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L481-L504) (which are common with others LaTeX' IDEs), such as `%! TeX program = xelatex` to specify a latex compiler. * For machine-aided proofreading, you can also enable [grammar checking tools](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L5577-L5610), such as [languagetool](https://languagetool.org/). I didn't check for [grammalecte](https://grammalecte.net/) support for French yet, but it may prove to be an [interesting endeavour](https://upload.wikimedia.org/wikipedia/commons/9/9f/Rabbit_Hole.jpg). **Remark.** vimtex [claims](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L6549-L6624) that their coloration is more accurate than what [tree-sitter](https://tree-sitter.github.io/tree-sitter/), then if you are using [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter), you may want to disable it for vimtex (it raises a warning otherwise): ```lua require("nvim-treesitter.configs").setup({ highlight = { enable = true, disable = { "latex", }, }, }) ``` Okay, that's all and good, but to quote [wikipedia](https://en.wikipedia.org): > The goal of the [language server] protocol is to allow programming language > support to be implemented and distributed independently of any given editor or > IDE. In the early 2020s LSP quickly became a "norm" for language intelligence > tools providers. Source: We are not early 2020s-ready for LaTeX yet, and even if we can send our current location to `zathura`, the contrary is not possible yet. Let us now address these two issues. # Language Server Protocol Setting up language server protocol with Vim is a big morsel, and have been the topic of [some tuppervim's sessions](https://tuppervim.org/archives/pads/grenoble-2212.txt) at some point. Here follows a minimal configuration that should work with `texlab`: ```lua -- Minimal lsp config local lspconfig = require("lspconfig") lspconfig.texlab.setup {} ``` Easy, innit? Well, that's well and good, we can now see errors and warnings decorating the file like a Christmas tree, but we can not use any of the LSP tools such as obtaining information on a bibliography key, or rename a macro. However, let us just remark that texlab is a pretty minimal LSP server, and does not implement the myriads of possible functionalities. Henceforth, I simply copy-pasted the default example from the [nvim-lspconfig Readme](https://github.com/neovim/nvim-lspconfig), tried the shortcuts one by one, and removed those which raised an error for “not implemented functionality” 🤡: ```lua -- Use LspAttach autocommand to only map the following keys -- after the language server attaches to the current buffer vim.api.nvim_create_autocmd("LspAttach", { group = vim.api.nvim_create_augroup("UserLspConfig", {}), callback = function(ev) -- Enable completion triggered by vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" -- Buffer local mappings. -- See `:help vim.lsp.*` for documentation on any of the below functions local opts = { buffer = ev.buf } vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts) vim.keymap.set("n", "K", vim.lsp.buf.hover, opts) vim.keymap.set("n", "gR", vim.lsp.buf.rename, opts) vim.keymap.set("n", "gr", vim.lsp.buf.references, opts) end, }) ``` Which thus enables: * Omnicompletion using LSP (I won't elaborate on this point, either you use it or not, but if you're using it, it may be useful to leave. I personally don't). * Go to a definition, with `gd`, which can be a macro, a reference, or even a bibliography reference. * Show the information about the element under the cursor using `K`, it can be useful to quickly check a reference. Note that pressing `K` twice jumps into the floating window. That can prove useful to copy an article title to search for it somewhere else for instance. * Rename a macro/variable among **all** files in the current working document using `gR`. It's a lifesaver when renaming macros as it avoids writing [regular expressions](https://xkcd.com/1171/). * Show each place where a reference appears with `gr` in a [quickfix](https://vimhelp.org/quickfix.txt.html) window. It allows checking where a formula is referenced or verifying if you cited yourself enough. I personally use [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) for that purpose as it is more readable, but it goes beyond the scope of this blogpost. And that is about it. We now follow the same steps as before: enable the completion engine by fetching the configuration from the [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) readme file and the [vimtex documentation](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L4586-L4625), then prune it. ```lua -- nvim-cmp local cmp = require("cmp") cmp.setup({ sources = cmp.config.sources({ { name = "nvim_lsp" }, { name = "buffer" }, }), mapping = cmp.mapping.preset.insert({ [""] = cmp.mapping.complete(), [""] = cmp.mapping.scroll_docs(-4), [""] = cmp.mapping.scroll_docs(4), [""] = cmp.mapping.confirm({ select = true }), }), }) ``` And we're all good from Neovim's side. You can of course start fine-tuning it but it's not the purpose of this blogpost. # Plug it into zathura Now that you tweaked your Neovim configuration so much that it now consumes 10GB of memory and takes 12s to launch using all your 24 CPU cores, we can move onto configuring zathura. One of the reasons I moved from [vim-latex](https://github.com/vim-latex/vim-latex) to [vimtex](https://github.com/lervag/vimtex) is reverse search: to enable it with `vim-latex`, I was using [nvim-remote](https://github.com/mhinz/neovim-remote) which is a wrapper for `nvim --listen` with a lot of constraints, while the most annoying one is that if I used reverse search from a detached[^1] zathura window without starting `nvr` first… then it spawns the process which I cannot recover. Which usually happens when I'm in a rush to fix something quickly. Fortunately, this is a thing of the past as it is possible to directly send a directive to `vimtex` upon which it will look for the corresponding buffer and open the file at the corresponding location while following its state (which can be viewed with `,li`). To do so, the [documentation](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L5985-L6033) states that you have to launch the following command, where `%l` is the line in the file and `%f` is the name of the file: ```bash nvim --headless -c "VimtexInverseSearch %l '%f'" ``` That's all and good, we just have to tell Zathura which command to launch when doing backward search, which by default is done with `Ctrl` + `left mouse button` on the portion of the text you want to view in the code. To do that, the following configuration that you can put in `$HOME/.config/zathura/zathurarc` should do the trick: ``` set synctex true set synctex-editor-command "nvim --headless -c \"VimtexInverseSearch %{line} '%{input}'\"" ``` And… that's it! You can now go to the location you want in your source file, compile it on the fly and scrutinise the warnings to look for overfull hboxes! [^1]: Meaning that it is not owned by any terminal I have opened, I can otherwise still recover it somehow. # Conclusion In this blogpost, we saw how to minimally set up Neovim to work with latex using modern toolchains. You can use it as a base to then improve your workflow and write your documents in a breeze with neovim. To summarise the configuration we used, it can be done in an `init.lua` file in your vim configuration directory: ```lua -- Lazy Package Manager local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" if not vim.loop.fs_stat(lazypath) then vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", -- latest stable release lazypath, }) end vim.opt.rtp:prepend(lazypath) require("lazy").setup({ "lervag/vimtex", "neovim/nvim-lspconfig", "hrsh7th/cmp-nvim-lsp", "hrsh7th/nvim-cmp", }) -- vimtex vim.g.vimtex_view_method = "zathura" vim.g.maplocalleader = "," vim.o.foldmethod = "expr" vim.o.foldexpr="vimtex#fold#level(v:lnum)" vim.o.foldtext="vimtex#fold#text()" vim.o.foldlevel=2 -- Minimal lsp config local lspconfig = require("lspconfig") lspconfig.texlab.setup {} -- Use LspAttach autocommand to only map the following keys -- after the language server attaches to the current buffer vim.api.nvim_create_autocmd("LspAttach", { group = vim.api.nvim_create_augroup("UserLspConfig", {}), callback = function(ev) -- Enable completion triggered by vim.bo[ev.buf].omnifunc = "v:lua.vim.lsp.omnifunc" -- Buffer local mappings. -- See `:help vim.lsp.*` for documentation on any of the below functions local opts = { buffer = ev.buf } vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts) vim.keymap.set("n", "K", vim.lsp.buf.hover, opts) vim.keymap.set("n", "gR", vim.lsp.buf.rename, opts) vim.keymap.set("n", "gr", vim.lsp.buf.references, opts) end, }) -- nvim-cmp local cmp = require("cmp") cmp.setup({ sources = cmp.config.sources({ { name = "buffer" }, { name = "nvim_lsp" }, }), mapping = cmp.mapping.preset.insert({ [""] = cmp.mapping.complete(), [""] = cmp.mapping.scroll_docs(-4), [""] = cmp.mapping.scroll_docs(4), [""] = cmp.mapping.confirm({ select = true }), }), }) ``` and the following in your `zathurarc` file: ``` set synctex true set synctex-editor-command "nvim --headless -c \"VimtexInverseSearch %{line} '%{input}'\"" ``` Note that due to some technical limitations, it's not fully perfect. For instance, synctex is not fully accurate with beamer slides, and just select the whole slide instead of the selected text. It is still better than nothing in my opinion, and it's a drawback that every LaTeX IDE is subject to. Now that everything is set up, you can skim the [vimtex documentation](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt) to look for things you want to activate and learn more about its features. Keep in mind though that you should not be too greedy, just pick some habits one at a time in order to ingrain them into your workflow. You can also expand directly `Neovim` with [snippets](https://github.com/L3MON4D3/LuaSnip) support for instance to automate some tasks as LaTeX can be quite verbose from time to time. I leave you now with some further reading about the topic. ## See Also * jdhao. [A Complete Guide on Writing LaTeX with Vimtex in Neovim](https://jdhao.github.io/2019/03/26/nvim_latex_write_preview/). June 2019. A blogpost that serves the same purpose as this one, eventhough it's not fully up to date, for instance regarding backward search. * Gilles Castel. [How I'm able to take notes in mathematics lectures using LaTeX and Vim](https://castel.dev/post/lecture-notes-1/). An example of how to streamline writing maths with Neovim, vimtex and snippets. The goal may not be for everyone (as writing new maths and following a lecture are not one and the same), but it's still an interesting read. For instance it presents the `concealment` feature of [vim](https://www.vim.org/) that makes previewing the result easier. I also recommend the rest of the blog, as it contains information about [inkscape](https://inkscape.org/) and how to integrate it with LaTeX. # Bonus: Key bindings for bépo users As a [bépo](https://fr.wikipedia.org/wiki/B%C3%A9po) user, I have some remapping done in Neovim, and especially [direction keys](https://vimhelp.org/usr_02.txt.html#02.3): ```lua -- Some shortcuts local keymap = vim.keymap.set local opts = {noremap = true, silent = true} -- [HJKL] <-> {CTSR} local map_list = { ['c'] = 'h', ['r'] = 'l', ['t'] = 'j', ['s'] = 'k', ['C'] = 'H', ['R'] = 'L', ['T'] = 'J', ['S'] = 'K', -- [HJKL] -> [CTSR] ['j'] = 't', ['J'] = 'T', ['l'] = 'c', ['L'] = 'C', ['h'] = 'r', ['H'] = 'R', ['k'] = 's', ['K'] = 'S', -- [CTSR] -> [HJKL]: J = until, L = change, h = replace, k = substitute } for key, binding in pairs(map_list) do keymap({'n', 'x'}, key, binding, opts) end ``` That's nice and all but… it conflicts with the [vimtex default mappings](https://github.com/lervag/vimtex/blob/master/doc/vimtex.txt#L800-L912) such as `cse` to rename an environment which can be useful to replace an `align` with `align*` for instance. Meaning that going back one character would trigger vim to wait for the next key input, which is kind of annoying. Hence the need to remap the vimtex default shortcuts starting with `c`, `t`, `s` or `r`. Fortunately, it's only the case for `c` and `t`. I first just add the remapping to `$NVIMDIR/after/ftplugin/tex.lua`, however I soon noticed that it's not sufficient as vimtex is also used for `.tikz`, `.cls` and `.bib` files,[^2] thus we will use [autocommand](https://neovim.io/doc/user/lua-guide.html#lua-guide-autocommands) for that: ```lua -- Some BÉPO mappings for vimtex vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { pattern = {"*.tex", "*.bib", "*.cls", "*.tikz",}, group = vim.api.nvim_create_augroup("latex", { clear = true }), callback = function() local vimtex_remaps = { -- c <-> t { mode = "n", source = "csd", target = "tsd", command = "(vimtex-delim-change-math)"}, { mode = "n", source = "csc", target = "lsc", command = "(vimtex-cmd-change)"}, { mode = "n", source = "cse", target = "lse", command = "(vimtex-env-change)"}, { mode = "n", source = "cs$", target = "ls$", command = "(vimtex-env-change-math))"}, -- t <-> j { mode = {"x", "n"}, source = "tsD", target = "jsD", command = "(vimtex-delim-toggle-modifier-reverse)"}, { mode = {"x", "n"}, source = "tsd", target = "jsd", command = "(vimtex-delim-toggle-modifier)"}, { mode = {"x", "n"}, source = "tsf", target = "jsf", command = "(vimtex-cmd-toggle-frac)"}, { mode = "n", source = "tsc", target = "jsc", command = "(vimtex-cmd-toggle-star)"}, { mode = "n", source = "ts$", target = "js$", command = "(vimtex-env-toggle-math)"}, { mode = "n", source = "tse", target = "jse", command = "(vimtex-env-toggle-star)"}, } for _,remap in pairs(vimtex_remaps) do if vim.fn.maparg(remap.source) ~= "" then vim.keymap.del(remap.mode, remap.source, { buffer = true }) vim.keymap.set(remap.mode, remap.target, remap.command, { silent = true, noremap = true, buffer = true}) end end end, }) ``` The sanity check with [`maparg(·)`](https://vimhelp.org/builtin.txt.html#maparg%28%29) is done to avoid unmapping a mapping that already doesn't exist, which will raise an error (as I have the (bad?) habit to type `:e` to reload the current file when thinking, that what triggered this behaviour in my case). **Remark.** Please also note that it is not fully remapped yet, for instance in the table of content navigation there are still collisions, as `t` for instance toggles showing TODOs or `s` toggles the section numbering. To finish and for the sake of completeness, here follows the bépo-bindings for zathura, to put in your `zathurarc` file: ```text ## BEPO # hjkl → ctsr map t scroll down map s scroll up map c scroll left map r scroll right # JK → TS map T navigate next map S navigate previous # r → p map p rotate rotate-cw # R → u map u reload # Mode Index map [index] t navigate_index down map [index] s navigate_index up map [index] r navigate_index expand map [index] c navigate_index collapse map [index] R navigate_index expand-all map [index] C navigate_index collapse-all ``` [^2]: Actually `.cls` and `.tikz` are detected as tex files, so the `ftplugin` approach works but `.bib` is detected as a bibtex file and enjoys its own filetype.