很长时间以来,我都在以同步的方式格式化代码,而这么做的唯一原因就是实现简单,我只需要在文件保存前的 BufWritePre
事件中以阻塞的方式调用 vim.lsp.buf.format()
即可轻松完成,就像这样:
vim.api.nvim_create_autocmd("BufWritePre", {
group = group,
buffer = bufnr,
callback = function() vim.lsp.buf.format { bufnr = bufnr } end
})
但它有个致命问题 —— 在格式化时会阻塞 UI。这意味着每次保存文件,都需要等上一阵才能继续其它操作。
随着文件体积越来越大,格式化所需时间越来越长,我也越来越讨厌这种方式。
有趣的是,vim.lsp.buf.format()
有个 async
选项,打开它,可以避免阻塞,以异步的方式格式化代码 —— 这才是正确的方式,格式化是你 formatter 的事,阻塞我 nvim 干啥。
但 vim.lsp.buf.format({ async = true })
真的就只是“以异步的方式格式化代码”,它并不提供像 buffer 锁定这种保护机制。
这表明,如果你改了 buffer 内容,在格式化完成时,它会直接粗暴地将做出的更改丢弃,进而覆盖为格式化结果。
lsp-format.nvim 在 vim.lsp.buf.format({ async = true })
的基础上,增加了 buffer 锁定 —— 格式化完成时,若 buffer 已经变更,则丢弃格式化结果,避免将其覆盖。这点很符合我的 workflow,这次没赶上格式化就等下次,反正总有一次能赶上,我只需要保证我的文件样式“最终一致”。
到这里,我已经觉得 lsp-format.nvim 完全能满足我的需求了,还将其用更简练的代码重新实现了一版,主要是去除了多 LSP 支持,因为我不认为会用到它 —— 直到我遇见了 ESLint、Prettier 这对冤家。
ESLint 与 Prettier 在格式化这点上,有大量功能重叠,照常来说,只使用 Prettier 作为 formatter 就够了,ESLint 只负责报告 diagnostics,和提供 code actions。
但 Prettier 过于固执,可供开发者配置的选项少得可怜。于是我尝试使用 ESLint 完全替换 Prettier,这样我仍能仅使用一个 LSP 完成 format,但在逐个配完 200+ 条 formatting rules 后,我放弃了。
目前,就我而言,会同时使用 ESLint、Prettier 两个作为 formatter,先过一遍 Prettier 作为 baseline,再过一遍 ESLint 调整某些更加细微的样式。当然,这也取决于项目配置,若未配置 ESLint 则没有后面这步。
lsp-format.nvim 支持多 LSP,但实现却很糙。具体来说,它只是依次请求每个 LSP,LSP 之间则简单地使用 buffer 传递上个格式化结果,这就会导致非预期的中间结果被看到,像这样:
Prettier 先是为 setPosts
参数加了换行,再由 ESLint 将返回的各个属性对其。这是不可接受的,我期望能一步到位,像这样:
可能有人会说,给属性加空格对其是邪恶的!因为属性名变更后,可能会影响空格数量的变化,从而影响到其它行,这会给 code review 带来麻烦。
的确,但还容我辩解下:
诚然,要解决这个问题可以通过 eslint-config-prettier 使 ESLint 部分规则与 Prettier 表现一致,这也是大多数人在做的。但这只是一种妥协,毕竟它们真的不一样。况且将多种工具很好地整合在一起,这本身应该是 editor 的责任。
读了下 Neovim LSP 实现,发现这根本不可能!也终于明白了为什么 lsp-format.nvim 使用 buffer 传递格式化结果,因为 nvim 的 LSP 和 buffer 强绑定,每次 format 请求都需要、且仅需要传递 bufnr
:
client.request(
"textDocument/formatting",
vim.lsp.util.make_formatting_params(),
function(err, result) end,
bufnr
)
可以看到,根本没有传递“待格式化代码”的参数,只有一个 bufnr
。这还不是关键,关键是,格式化完返回的 result
也只是代码中需要替换的部分,而不是完整的格式化结果,像这样:
{
{
newText = "..."
range = {
["end"] = { character = 7, line = 15 },
start = { character = 7, line = 15 }
}
},
{
newText = "...",
range = {
["end"] = { character = 10, line = 16 },
start = { character = 10, line = 16 }
}
},
-- ...
}
该结果只能通过 vim.lsp.util.apply_text_edits()
处理,让人崩溃的是,它的参数也只是 bufnr
:
vim.lsp.util.apply_text_edits(result, bufnr, client.offset_encoding)
调用后 buffer 会发生实质的变化。什么,你想在不改变 buffer 的前提下,得到格式化结果?门儿都没有!
于是,我选择重写它们:
client.request()
不能用,用更底层的 client.rpc.request()
,它是与 language server 建立的最原始的连接client.rpc.request()
必须手动发送 textDocument/didChange
通知vim.lsp.util.apply_text_edits()
,在内存中计算格式化后的完整结果,不变更 buffer完整实现见:https://github.com/sxyazi/dotfiles/blob/main/nvim/lua/formatter.lua
如果这 300 来行对你有用,请告诉我!我会考虑将其从我的配置中抽离,制成一个真正的插件,并创建一个新的 repo 维护它。