OCaml 的 Neovim 配置方案

信息

更新说明:

  • 2024-09-14. 部分 LSP 本身包含 Formatter,就不需要自己下载 Formatter 再用 null-ls 处理。通过修改 vim.lsp.buf.formatfilter 参数就可以让这两种情况并存

上一篇博客中,我详细介绍了如何从零开始配置 Neovim,让它更接近 IDE。但我没有讲到的是:

  1. 有的 LSP 本身不支持代码格式化,要如何配置 Neovim 让它可以用第三方的代码格式化工具?
  2. 新增一门编程语言支持的时候,要怎么修改配置文件?

最近在学习函数式编程需要在我的 Mac 上配置 OCaml,正好以它为例,回答以上两个问题

信息
本文假定你已经阅读过我之前的文章,因此对于一些细节问题,我就不再赘述
信息
其他系统的安装方法可以看这里

在 Mac 上安装 OCaml 用 Homebrew 比较方便

sh

$ brew install opam

按照 Homebrew 的输出,我们还需要进行额外的配置,需要修改 ~/.zshrc(如果你用的是 Zsh)或者是 ~/.bashrc(如果你用的是 Bash)

sh

$ opam init
# 按照提示按下 y 确认即可

接下来让我们上一步的配置生效

sh

$ source ~/.zshrc
# $ source ~/.bashrc # if you use bash

最后,我们可以通过如下的命令检查是否安装成功

sh

$ opam switch

输出应该跟下面的差不多

text

#   switch   compiler                   description
->  5.0.0    ocaml-base-compiler.5.0.0  5.0.0
    default  ocaml.4.14.0               default
技巧

接下来需要安装一些包,因为网络原因访问可能会比较慢。加速可以通过换源实现,这里用的是上海交通大学的源,配置命令为

sh

$ opam repo \
    set-url default \
    https://mirrors.sjtug.sjtu.edu.cn/git/opam-repository.git \
    --all --set-default

等待该命令执行完就完成了换源

类似 Python 有 REPL,如 IPython。在 OCaml 里也有类似的工具,叫做 utop

sh

$ opam install utop

接下来尝试使用 utop 看是否安装成功

sh

$ utop

如果一切正常,你应该可以看到如下的输出

text

─────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────
                                                          Welcome to utop version 2.13.1 (using OCaml version 5.0.0)! 
                                                         └─────────────────────────────────────────────────────────────┘

Type #utop_help for help about using utop.

( 10:33:26 )< command 0 >────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────{ counter: 0 }
utop #
┌──────────────┬──────────────┬───────────────┬─────┬───────────┬────┬───┬──────────┬─────┬───────────┬──────┬────────────┬───────┬───────────┬──────────────┬──────────┬────────┐
Afl_instrumentAlias_analysisAllocated_constAnnotApplicativeArchArgArg_helperArrayArrayLabelsAsmgenAsmlibrarianAsmlinkAsmpackagerAssert_failureAst_helperAst_inva
└──────────────┴──────────────┴───────────────┴─────┴───────────┴────┴───┴──────────┴─────┴───────────┴──────┴────────────┴───────┴───────────┴──────────────┴──────────┴────────┘
信息

如果你跟着我上一篇的教程进行配置,那么你的目录结构应该长下面这样

text

nvim
├── init.lua
└── lua
    ├── colorscheme.lua
    ├── config
    │   └── nvim-cmp.lua
    ├── keymaps.lua
    ├── lsp.lua
    ├── options.lua
    └── plugins.lua

之前的文章里面我们配置 LSP 的时候借助了 mason.nvim。要增加新的编程语言的 LSP 同样可以用这个工具,步骤如下

  1. 打开 Neovim,输入 :Mason,在 (2) LSP 里面找到 ocaml-lsp,将光标放在该行然后按下 i 就可以安装了,稍候片刻☕️
  2. 打开 LSP 配置文件(我放在了 lsp.lua 文件里面),添加如下的内容

lua

... -- rest of the configurations
lspconfig.ocamllsp.setup({
	on_attach = on_attach,
})
警告
你可能有自己的配置文件而没有跟着我的上一篇 文章操作,所以在你的配置文件里面加上这一段代码并不保证能够 work。你可以选择浏览完整文件 来理解这段代码的作用,和尝试迁移配置到你的配置文件中

修改完 lsp.lua 文件之后,重启 Neovim 之后就生效了

警告
rust-analyzer 类似,ocaml-lsp 的 LSP 也只支持在项目里启用,无法在单文件下生效

接下来,可以用 dune 新建项目,尝试写一下 OCaml 的代码,检查 LSP 是否正常工作

ocaml-lsp 本身并不支持 OCaml 代码的格式化,在完成如上的配置之后,如果你尝试格式化,会发现 Neovim 报错:

text

[LSP] Format request failed, no matching language servers.

因此,接下来要讲解的就是如何配置 OCaml 的代码格式化功能。

之前你可能已经注意到,在 Neovim 里面输入 :Mason 的时候可以看到,它不仅支持安装 LSP,还可以安装 Linter、Formatter 等。其中 Formatter 就是用于代码的格式化。OCaml 的 Formmater 是 ocamlformat,首先我们在 Mason 的窗口 (5) Formatter 里找到它,将光标停留在该选项,然后按下 i 安装。等待片刻之后就可以看到安装成功了☕️

ocamlformat 本身只是一个 CLI 工具,没法方便地集成到 Neovim 里面。好在还有 none-ls 插件,它可以将 Linter、Formatter 或者命令行等工具暴露成 LSP 给 Neovim

信息
none-ls.nvim 的前身是 null-ls.nvim,但是 null-ls 的开发者已经不再继续更新了,因此社区的热心开发者维护了另外一个功能一样的版本叫做 none-ls

为了进一步减轻配置的负担,我们可以利用 mason-null-ls.nvim 插件,从名字可以看出来,它是 mason.nvim 的开发者开发的。打开 plugins.lua 新增如下 Lua 代码,保存并重启后 lazy.nvim 就会自动安装新插件,稍候片刻☕️

lua

... -- rest of the configurations
require("lazy").setup({
	-- Add hooks to LSP to support Linter && Formatter
	{
		"jay-babu/mason-null-ls.nvim",
		event = { "BufReadPre", "BufNewFile" },
		dependencies = {
			"williamboman/mason.nvim",
			"nvimtools/none-ls.nvim",
		},
		config = function()
			-- Note:
			--     the default search path for `require` is ~/.config/nvim/lua
			--     use a `.` as a path seperator
			--     the suffix `.lua` is not needed
			require("config.mason-null-ls")
		end,
	},
    ... -- rest of the configurations
})

然后在 config 文件夹里新建 mason-null-ls.lua 文件,内容如下

lua

local mason_ok, mason = pcall(require, "mason")
if not mason_ok then
	return
end

local null_ls_ok, null_ls = pcall(require, "null-ls")
if not null_ls_ok then
	return
end

local mason_null_ls_ok, mason_null_ls = pcall(require, "mason-null-ls")
if not mason_null_ls_ok then
	return
end

mason.setup()

mason_null_ls.setup({
	-- A list of sources to install if they're not already installed.
	-- This setting has no relation with the `automatic_installation` setting.
	ensure_installed = {},
	-- Run `require("null-ls").setup`.
	-- Will automatically install masons tools based on selected sources in `null-ls`.
	-- Can also be an exclusion list.
	-- Example: `automatic_installation = { exclude = { "rust_analyzer", "solargraph" } }`
	automatic_installation = false,
	-- Sources found installed in mason will automatically be setup for null-ls.
	automatic_setup = true,
	handlers = {
		-- Hint: see https://github.com/nvimtools/none-ls.nvim/blob/main/doc/BUILTIN_CONFIG.md
		--       to see what sources are available
		-- Hint: see https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/doc/BUILTIN_CONFIG.md
		--       to check what we can configure for each source
		ocamlformat = function(source_name, methods)
			null_ls.register(null_ls.builtins.formatting.ocamlformat.with({
				-- Add more arguments to a source's defaults
				-- Default: { "--enable-outside-detected-project", "--name", "$FILENAME", "-" }
				-- type `ocamlformat --help` in your terminal to check more args
				extra_args = { "--if-then-else", "vertical" },
			}))
		end,
	},
})

null_ls.setup()
技巧

当你开启 automatic_setup = true 的时候,mason-null-ls 会自动帮你进行配置安装了的 Formatter。如果想要修改默认配置,就需要在 handlers 里面进行修改

FAQs

  1. 如何找到对应 Formatter 的默认配置?看 这里
  2. 如何配置 handlers?看 这里
  3. 如何知道对应的 Formatter 其他可选配置选项?看对应 LSP 的官方 GitHub 仓库。比如 ocamlformat 就在文档说了,在命令行输入 ocamlformat --help 就可以看到各种配置项

最后,我们还需要修改 lsp.lua 文件,原本跟代码格式化有关的行长这个样子

lua

...
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({ async = true })
end, bufopts)
...

现在因为我们使用 none-ls,相当于所有的格式化都交给它来做,因此,需要新增 filter 参数

lua

... -- rest of the configurations
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({
        async = true,
		-- Predicate used to filter clients. Receives a client as
		-- argument and must return a boolean. Clients matching the
		-- predicate are included.
        filter = function(client)
            return client.name == "null-ls"
        end,
    })
end, bufopts)
... -- rest of the configurations

完成文件编辑之后,重启 Neovim 就生效了

技巧

那如果一个 LSP 本身就包含了 Formatter 呢?比如 Haskell-language-server,那么我们就不需要null-ls 进行格式化,通过修改 filter 参数就可以解决,像下面这样

lua

... -- rest of the configurations
vim.keymap.set("n", "<space>f", function()
    vim.lsp.buf.format({
        async = true,
		-- Predicate used to filter clients. Receives a client as
		-- argument and must return a boolean. Clients matching the
		-- predicate are included.
        filter = function(client)
            -- hls stands for Haskell-language-server
			return client.name == "null-ls" or client.name == "hls"
        end,
    })
end, bufopts)
... -- rest of the configurations

现在对前文的内容进行总结,如果你跟我一样也使用 mason.nvim, mason-lspconfig, none-ls.nvimmason-null-ls.nvim 这些插件。那么在添加一门新的编程语言的时候,你可以这样配置:

  1. 首先在你的机器上配置好该编程语言的开发环境,查询官方文档即可
  2. 打开 Neovim,输入 :Mason 安装对应的 LSP,先检查官方文档看有没有自带 Formatter,如果没有就用 :Mason 安装对应的 Formatter
  3. 打开 lsp.lua 文件配置 LSP
  4. 如果是自己单独下载的 Formatter,还需要打开 mason-null-ls.lua 文件修改 handlers 参数

对于本文没有说清楚的地方,你可以查看我的 dotfiles 配置 来了解更多的信息~~