Add nvim-latex article (in English)

This commit is contained in:
Fabrice Mouhartem 2023-10-14 12:30:07 +02:00
parent d2c5efb43a
commit 6505b22d90
2 changed files with 538 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -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 `<LocalLeader>ll`. We now have to define
[`<LocalLeader>`](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 : `<F6>/<F7>` (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: <https://en.wikipedia.org/wiki/Language_Server_Protocol>
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 <c-x><c-o>
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({
["<C-Space>"] = cmp.mapping.complete(),
["<C-u>"] = cmp.mapping.scroll_docs(-4),
["<C-d>"] = cmp.mapping.scroll_docs(4),
["<C-l>"] = 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 <c-x><c-o>
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({
["<C-Space>"] = cmp.mapping.complete(),
["<C-u>"] = cmp.mapping.scroll_docs(-4),
["<C-d>"] = cmp.mapping.scroll_docs(4),
["<C-l>"] = 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 = "<Plug>(vimtex-delim-change-math)"},
{ mode = "n", source = "csc", target = "lsc", command = "<Plug>(vimtex-cmd-change)"},
{ mode = "n", source = "cse", target = "lse", command = "<Plug>(vimtex-env-change)"},
{ mode = "n", source = "cs$", target = "ls$", command = "<Plug>(vimtex-env-change-math))"},
-- t <-> j
{ mode = {"x", "n"}, source = "tsD", target = "jsD", command = "<Plug>(vimtex-delim-toggle-modifier-reverse)"},
{ mode = {"x", "n"}, source = "tsd", target = "jsd", command = "<Plug>(vimtex-delim-toggle-modifier)"},
{ mode = {"x", "n"}, source = "tsf", target = "jsf", command = "<Plug>(vimtex-cmd-toggle-frac)"},
{ mode = "n", source = "tsc", target = "jsc", command = "<Plug>(vimtex-cmd-toggle-star)"},
{ mode = "n", source = "ts$", target = "js$", command = "<Plug>(vimtex-env-toggle-math)"},
{ mode = "n", source = "tse", target = "jse", command = "<Plug>(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.