LaTeX 写作常常陷入两难:纯手打效率太低,IDE 又太过臃肿。本文记录笔者在 Neovim 中搭建 LaTeX 写作环境的完整过程,涵盖编译预览、代码片段、语法补全等环节,目标是让写作体验足够顺手。全文以 lazy.nvim 为插件管理器,适合已经会用 Neovim 但还没搭好 LaTeX 工作流的读者。
使用lazy.nvim作为插件管理器,如使用其他插件管理器,需要适当修改配置代码
准备工作
步骤
配置 LSP + 补全引擎
笔者使用的是最主流的mason+mason-lspconfig+nvim-lspconfig三个插件来管理 LSP 的下载、配置、接入;补全引擎为blink.cmp。由于这个部分不是本文重点,仅仅放出笔者的配置:
return {
{
"mason-org/mason-lspconfig.nvim",
opts = {
ensure_installed = { "clangd", "lua_ls", "rust_analyzer", "pyright", "ruff", "texlab", "marksman", "gopls" },
},
dependencies = {
{ "mason-org/mason.nvim", opts = {} },
"neovim/nvim-lspconfig",
},
},
{
"saghen/blink.cmp",
-- optional: provides snippets for the snippet source
-- dependencies = { "rafamadriz/friendly-snippets" },
-- use a release tag to download pre-built binaries
version = "1.*",
-- AND/OR build from source, requires nightly: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust
-- build = 'cargo build --release',
-- If you use nix, you can build from source using latest nightly rust with:
-- build = 'nix run .#build-plugin',
---@module 'blink.cmp'
---@type blink.cmp.Config
opts = {
-- 'default' (recommended) for mappings similar to built-in completions (C-y to accept)
-- 'super-tab' for mappings similar to vscode (tab to accept)
-- 'enter' for enter to accept
-- 'none' for no mappings
--
-- All presets have the following mappings:
-- C-space: Open menu or open docs if already open
-- C-n/C-p or Up/Down: Select next/previous item
-- C-e: Hide menu
-- C-k: Toggle signature help (if signature.enabled = true)
--
-- See :h blink-cmp-config-keymap for defining your own keymap
keymap = {
preset = "default",
["<Tab>"] = {
function(cmp)
local ls = require("luasnip")
if ls.expandable() then
cmp.cancel()
vim.schedule(function()
ls.expand()
end)
return true
end
end,
"snippet_forward",
"fallback",
},
["<CR>"] = { "accept", "fallback" },
["<C-j>"] = { "show", "show_documentation", "hide_documentation" },
},
appearance = {
-- 'mono' (default) for 'Nerd Font Mono' or 'normal' for 'Nerd Font'
-- Adjusts spacing to ensure icons are aligned
nerd_font_variant = "mono",
},
-- (Default) Only show the documentation popup when manually triggered
completion = { documentation = { auto_show = true } },
-- Default list of enabled providers defined so that you can extend it
-- elsewhere in your config, without redefining it, due to `opts_extend`
snippets = { preset = "luasnip" },
sources = {
default = { "lsp", "path", "snippets", "buffer" },
},
-- (Default) Rust fuzzy matcher for typo resistance and significantly better performance
-- You may use a lua implementation instead by using `implementation = "lua"` or fallback to the lua implementation,
-- when the Rust fuzzy matcher is not available, by using `implementation = "prefer_rust"`
--
-- See the fuzzy documentation for more information
fuzzy = { implementation = "prefer_rust_with_warning" },
signature = { enabled = true },
},
opts_extend = { "sources.default" },
},
}
VimTeX
VimTeX 提供了丰富的文本对象、高亮、环境检测和编译集成等功能。配置代码如下:
return {
{
"lervag/vimtex",
lazy = false,
init = function()
vim.g.vimtex_syntax_enabled = 1 -- 使用vimtex提供的高亮
vim.g.vimtex_view_method = "zathura" -- 使用zathura打开pdf
end
}
}
具体的功能和用法,读者可以自行研究,下面介绍几例笔者常用的功能:
- 开启自动编译:保存文件后自动编译,默认映射为
<LocalLeader>ll(LocalLeader 默认为\);编译完成后用<LocalLeader>lv正向搜索,在 Zathura 中定位到光标所在位置对应的 PDF 片段;在 Zathura 中按Ctrl+Click可以反向搜索,跳回 tex 源码对应行。 - 显示目录:
<LocalLeader>lt打开浮动目录窗口,可以快速跳转到各个 section、label、引用和 TODO 项。 - 文本对象:VimTeX 提供了丰富的 text object,比如
ie选中环境内容、ae选中环境及其\begin/\end行、id选中$...$或\(...\)内联公式内容、am选中块级公式及其\[...\]边界。
LuaSnip
LuaSnip 提供代码片段补全功能,将常见结构预设为片段,能显著提高编辑速度和体验。配置代码如下:
return {
{
"L3MON4D3/LuaSnip",
version = "v2.*",
build = "make install_jsregexp",
config = function()
local ls = require("luasnip")
require("luasnip.loaders.from_vscode").lazy_load()
require("luasnip.loaders.from_lua").load({ paths = "./LuaSnip" })
require("luasnip").config.set_config({
enable_autosnippets = true,
store_selection_keys = "<Tab>",
})
vim.keymap.set({ "i", "s" }, "<C-f>", function()
if ls.choice_active() then
ls.change_choice(1)
end
end, { silent = true })
vim.keymap.set({ "i", "s" }, "<C-b>", function()
if ls.choice_active() then
ls.change_choice(-1)
end
end, { silent = true })
vim.keymap.set({ "i", "s" }, "<C-k>", function()
if ls.expandable() then
ls.expand()
end
end, { silent = true })
end,
},
}
在编写 snippet 文件时,通常会做如下的约定:
local ls = require("luasnip")
local s = ls.snippet
local sn = ls.snippet_node
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local d = ls.dynamic_node
local fmt = require("luasnip.extras.fmt").fmt
local fmta = require("luasnip.extras.fmt").fmta
local rep = require("luasnip.extras").rep
关于这个插件的具体配置和使用,笔者只介绍几例,挂一漏万,权当是抛砖引玉。
先看最简单的入门例子——一个将 alpha 展开为 \alpha 的 snippet:
s({ trig = "alpha" }, {
t("\\alpha"),
})
在插入模式下输入 alpha 后按展开键(前文配置的 <C-k>),\alpha 就会被插入。s() 的第一个参数是触发条件(这里只用了 trig),第二个参数是一个节点列表——这里只有一个 text_node,即纯文本。
- 自动展开的代码片段
掌握了基本写法后,接下来看如何利用 VimTeX 的 API 让 snippet 更智能。核心需求是:希腊字母补全只在公式环境内生效,在正文中不触发。只需:
local tex_utils = {}
tex_utils.in_mathzone = function() -- math context detection
return vim.fn["vimtex#syntax#in_mathzone"]() == 1
end
就可以在后续的配置中使用这个环境来让一些代码片段自动展开——这样 ;a 在正文中不会误触发,只在公式环境内自动展开为 \alpha:
s({ trig = ";a", snippetType = "autosnippet", condition = tex_utils.in_mathzone }, {
t("\\alpha"),
}),
s({ trig = ";b", snippetType = "autosnippet", condition = tex_utils.in_mathzone }, {
t("\\beta"),
}),
- 动态生成片段
利用 Lua 我们可以实现动态生成的代码片段,提升效率。
local function make_table(_, parent)
local rows = tonumber(parent.captures[1])
local cols = tonumber(parent.captures[2])
local nodes = {}
local k = 1
for r = 1, rows do
for c = 1, cols do
table.insert(nodes, i(k))
k = k + 1
if c ~= cols then
table.insert(nodes, t(" & "))
end
end
if r ~= rows then
table.insert(nodes, t({ " \\\\", "\t\t" }))
end
end
return sn(nil, nodes)
end
local function make_colspec(_, parent)
local cols = tonumber(parent.captures[2])
local nodes = {}
for c = 1, cols do
table.insert(nodes, t({ "X" }))
end
return sn(nil, nodes)
end
return {
s(
{ trig = "t(%d+)x(%d+)", hidden = true, regTrig = true, wordTrig = true, dscr = "Quick make a table" },
fmta(
[[
\begin{table}[htbp]
\centering
\caption{<>}
\begin{tblr}{
colspec={<>},
width=<>\linewidth,
hlines,
}
<>
\end{tblr}
\end{table}
]],
{
i(1),
d(2, make_colspec),
i(3),
d(4, make_table),
}
)
),
}
- 实现计算器
同样是利用 Lua 的数学库实现的计算功能,需要注意的是由于blink.cmp的补全机制,需要阻止提前运行,涉及的代码如下:
local function eval_math(_, parent)
-- Return placeholder during static/docstring resolution (blink.cmp resolve)
-- without blocking. parent or parent.snippet may be nil.
local in_buffer = pcall(function()
return parent.snippet:extmarks_valid()
end)
if not in_buffer then
return sn(nil, t(""))
end
-- Real expansion: prompt for input.
local expr = vim.fn.input("expr: ")
if expr == "" then
return sn(nil, t(""))
end
local env = { math = math }
for k, v in pairs(math) do env[k] = v end
env.sqrt = function(n, x)
if x then return x ^ (1 / n) end
return math.sqrt(n)
end
local fn, err = load("return " .. expr, "eval_ctx", "t", env)
if not fn then
return sn(nil, t(err or "invalid"))
end
local ok, result = pcall(fn)
if not ok then
return sn(nil, t("error"))
end
return sn(nil, t(tostring(result)))
end
return {
s(
{ trig = "calc", desc = "calculate the expression" },
fmta("<>", { d(1, eval_math) })
),
}
总结
以上是笔者 LaTeX 写作环境的核心组件:texlab + blink.cmp 提供补全,VimTeX 负责编译预览,LuaSnip 用片段削减重复劳动。三者配合,日常写作基本脱离鼠标。具体教程推荐 参考,感谢最初启发本文的 分享,以及上述开源项目的贡献者。