diff --git a/content/images/covers/fern-forest.jpg b/content/images/covers/fern-forest.jpg new file mode 100644 index 0000000..48d8cd6 Binary files /dev/null and b/content/images/covers/fern-forest.jpg differ diff --git a/content/software/nvim-latex.md b/content/software/nvim-latex.md new file mode 100644 index 0000000..6a74a91 --- /dev/null +++ b/content/software/nvim-latex.md @@ -0,0 +1,538 @@ +--- +Title: Neovim as a LaTex Development Environment +Date: 2023-10-14 12: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), allows easy +cross-referencing and automatically generates table of contents. + +It is based on a markup language that allows the 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://ctan.org/pkg/algorithm2e) 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: [backward and forward +searches](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. + +* 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` to be up to the +task. +We will assume a blank configuration and starts 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 install 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… +However, 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` after compilation, +which is by default bound to `ll`. We now have to define +[``](https://neovim.io/doc/user/map.html#%3CLocalLeader%3E), which +I usually set to “`,`”: +```lua +vim.g.maplocalleader = "," +``` + +Now, you can use `,lv` to view the current line in `zathura`, yay. + +More can be then done, such as using vimtex folds, which are not enabled 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… a + 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. + +**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 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 +sessions](https://tuppervim.org/archives/pads/grenoble-2212.txt) at some point. + +I'll present here a minimal configuration that should work with `texlab`: + +```lua +-- Minimal lsp config +local lspconfig = require("lspconfig") +lspconfig.texlab.setup {} +``` + +Okay, that's all and good, we can see errors and warnings decorating the file +like Christmas decorations, but we can't 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 +don't implement the myriads of possible functionalities. +Henceforth, I just copy-pasted the default example from the [nvim-lspconfig +Readme](https://github.com/neovim/nvim-lspconfig), tried the shortcuts one by +one, and remove these 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 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, which can be 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 documents + using `gR`. It's a lifesaver when renaming macros as it avoids writing [regular + expressions](https://xkcd.com/1171/). +* Show each places where a reference appears with `gr` in a quickfix 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's it, we now simply have to enable the completion engine getting 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 pruning 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 CPU cores, we can move to +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 is 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 which will look for the corresponding buffer and then open +the file at the right 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, now 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 file document, +compile it on the fly, 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. + +# 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 char would trigger vim +to wait for the next 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). + +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.