2023年,nvim以及其生态已经发展的愈来愈完善了。nvim内置的LSP(以及具体的语言服务)加上众多插件,可以搭建出支持各种类型语法检查、代码补全、代码格式化等功能的IDE。网络上关于如何配置的文章很多,但本人发现绝大多数的文章仅仅停留在配置本身,没有深入的解释这些插件的作用和它们之间的关系,这就导致了很多入门的小伙伴在配置、使用的过程中遇到各种问题也不知如何下手。本文将手把手,一步一步的演进并解释,帮助小伙伴了解这块的内容。
注意1:本文主要探讨nvim关于LSP、null-ls以及代码补全内容,不会详细介绍如何使用插件系统。
注意2:本文阅读前需要读者已经掌握了如何使用插件管理器来安装插件并setup插件配置。
认识LSP
在本文的开始,让我们先介绍一下LSP(Language Server Protocol,语言服务协议)。当然,网络上有很多详细的介绍LSP的内容,本文不会深入介绍它的实现机制,仅作为本文的入门的解释。
简单来讲,该协议定义了两端:Language Client(语言服务客户端)和Language Server(语言服务端),其核心是将代码编辑器文本界面的展示和代码语言分析(语言支持,自动补全,定义与引用解析等)解耦。通常,我们的文本编辑器就是一个客户端,而各种语言的解析则会有对应LSP协议实现的服务端。
为了让读者更加清楚的理解LSP的运作,我们编写有如下TypeScript代码:
// 1. 定义接口
interface User {
name: string;
}
// 2. 实现接口的对象
const user: User = {
name: 'hello'
}
// 3. 打印对象的age属性
console.log(user.age); // error
上述这段代码首先定义了一个名为User
的接口(interface User
),该接口拥有一个字段name
;然后,我们创建了一个基于User
接口的user实例;最后,我们打印了user的age属性。user并不具备age字段,所以按照严格的TypeScript语言规范来讲,代码编译肯定会有错误:
基于LSP的模型,我们可以将这个过程描述出来:
- 在编辑器上写入上述的TS代码;
- 编辑器将上述代码通过某种通讯协议发送给TypeScript语言服务器;
- TS语言服务读取TS代码,进行语法检查,得到了编译错误信息(包含行列数,基本的建议提示信息)返回给编辑器;
- 编辑器接收到错误信息,通过自己的方式展示在编辑器UI上。
现在,我们已经了解了基于LSP的代码分析处理流程,那么这个语言服务器在什么地方呢?首先,不要看到服务器三个字,就认为它一定是一个在远端的Web应用服务,语言服务器一般就是一个软件程序,只不过它能够处理专门解析你编写的程序代码,并做出响应。
使用LSP这套体系,有两个必备步骤:
- 获取并安装语言服务器程序;
- 启动语言服务器,让它处于运行状态。
有些语言服务器基于js编写实现,它一般是一个NPM包,我们以npm -g
全局安装的形式安装它(例如TypeScript的语言服务器的实现typescript-language-server
);有的语言服务器直接就是可执行程序(例如lua语言服务器lua-language-server
),我们从网络上下载它存放到电脑上。通常,我们会把它们的可执行文件路径加入到环境变量中,以便随时在命令行中启动它们。启动以后,它就在一个进程中默默的的等待着客户端(也就是编辑器)链接,并在建立连接以后,进行代码的分析处理工作。
nvim中的LSP
了解了LSP的基本概念以后,接下来我们介绍在nvim中的LSP模块。在nvim 0.5+版本以后,已经内置了语言服务客户端的接口(Lsp - Neovim docs,注意只是语言服务客户端部分),比较常用的API:
- vim.lsp.buf.hover():代码的TIPS悬浮展示。
- vim.lsp.buf.format():代码格式化。
- vim.lsp.buf.references():当前代码符号的引用查询。
- vim.lsp.buf.implementation():当前代码(主要是函数方法)的实现定位。
- vim.lsp.buf.code_action():当前代码的一些优化操作。
但需要注意,上述这些都是接口方法,它只是一个封装后的壳子方法,不具备具体的实现。具体的实现,需要为每一个编程语言单独配置。也就是说,nvim内置的lsp模块的运行架构如下:
面对不同的语言,我们按照对应的语言服务的要求来配置nvim的内置LSP模块。在官方的文档中给了如下的示例来启动一个LSP:
vim.lsp.start({
name = 'my-server-name',
cmd = {'name-of-language-server-executable'},
root_dir = vim.fs.dirname(vim.fs.find({'setup.py', 'pyproject.toml'}, { upward = true })[1]),
})
这段代码不过多赘述,因为它比起即将介绍的lspconfig插件来说,使用起来更加复杂。
nvim-lspconfig
每当有一个编程语言需要使用LSP的时候,我们都需要形如上述的nvim原生LSP配置来启动对应的语言服务器,同时还需要关注很多细节,譬如