首页 > 其他分享 >详解nvim内建LSP体系与基于nvim-cmp的代码补全体系

详解nvim内建LSP体系与基于nvim-cmp的代码补全体系

时间:2023-07-12 11:00:10浏览次数:43  
标签:插件 补全 代码 LSP nvim cmp

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语言规范来讲,代码编译肯定会有错误:

010-ts-type-error

基于LSP的模型,我们可以将这个过程描述出来:

  1. 在编辑器上写入上述的TS代码;
  2. 编辑器将上述代码通过某种通讯协议发送给TypeScript语言服务器;
  3. TS语言服务读取TS代码,进行语法检查,得到了编译错误信息(包含行列数,基本的建议提示信息)返回给编辑器;
  4. 编辑器接收到错误信息,通过自己的方式展示在编辑器UI上。

020-lang-server-error-check-workflow

现在,我们已经了解了基于LSP的代码分析处理流程,那么这个语言服务器在什么地方呢?首先,不要看到服务器三个字,就认为它一定是一个在远端的Web应用服务,语言服务器一般就是一个软件程序,只不过它能够处理专门解析你编写的程序代码,并做出响应。

使用LSP这套体系,有两个必备步骤:

  1. 获取并安装语言服务器程序;
  2. 启动语言服务器,让它处于运行状态。

有些语言服务器基于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模块的运行架构如下:

030-nvim-lsp-arch

面对不同的语言,我们按照对应的语言服务的要求来配置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配置来启动对应的语言服务器,同时还需要关注很多细节,譬如,你要手动启动它等等,这一点从用户体验上是比较不友好的。

为了更加方便快速的使用nvim的LSP模块,nvim官方提供了neovim/nvim-lspconfig这个插件。安装了这个插件以后,我们只需要进行少量且易于理解的配置,就能通过这个插件方便快捷的启动并使用语言服务了。

nvim-lspconfig通过插件管理器安装以后,我们就可以通过require的方式获取它,并通过它来配置某个编程语言的语言服务客户端。在lazy.nvim插件管理器下,配置如下:

040-nvim-lspconfig-demo-config

本人使用lazy.nvim来管理插件。上述第一行的"neovim/nvim-lspconfig"代表要安装该插件;紧接着的config需要编写一个函数,代表插件安装后的配置阶段的自定义运行过程(详见lazy.nvim的文档),这个方法在nvim每次启动后,会被lazy.nvim调用,我们一般会在这个config的回调方法中获取插件实例调用其相关API进行配置。

无论使用何种插件管理器,nvim-lspconfig的使用流程都是一样:

  1. 安装nvim-lspconfig插件(通过lazy.nvim、packer等插件管理器,甚至是纯手工安装);
  2. 在确保该插件安装完成后的某个时机,获取nvim-lspconfig插件实例(require('lspconfig')),这个插件实例可以访问不同编程语言的语言服务客户端对象(例如上面的 lspconfig['tsserver']),每一个语言服务客户端对象都会有setup方法,我们只需要通过这个方法传入对该语言的语言服务配置。

当然, 如果setup里面什么都不传,它会使用默认配置进行setup。像上面的lspconfig['tsserver'],它其实就是针对TypeScript代码的语言服务配置,默认配置如下:

050-nvim-lspconfig-tsserver-default-config

cmd代表了在我们机器上安装的语言服务器的命令行启动方式,比如在我们机器上启动TypeScript的语言服务,则会调用命令:typescript-language-server --stdio

filetypes代表了当遇到哪些文件类型的时候,会让语言服务建立连接。在本例中,只要你打开的文件类型是javascript、typescript等,就会建立编辑器客户端与语言服务的连接,连接完成以后,就能进行查看类型定义、格式化等语言处理操作了。

为了真的能启动语言服务器,我们按照文档提到的方式手动安装TypeScript和lua的语言服务器。在我的机器上,安装好以后,能够通过命令行方式访问得到:

060-ts-and-lua-ls-location

让我们来梳理下上述demo的现状:

  1. 我们使用了0.5版本以上的nvim,它拥有内建的支持LSP客户端的模块;
  2. 我们安装了nvim-lspconfig插件,并在通过配置,让它在加载以后,又去setup了TypeScript和lua的语言服务配置;
  3. 我们在电脑上外部安装了TypeScript和lua的语言服务器,能够通过命令行访问到。

步骤1、2保证了我们的nvim具备了成为语言服务客户端的能力;步骤3保证了我们的电脑环境安装了所需要的语言服务器。

此时,当我们打开一个TS代码的时候,命令模式下键入LspInfo,就会看到如下的信息:

070-ts-ls-attach-to-demo

弹出信息告诉我们,有一个tsserver关联到了当前buffer(也就是这个demo.ts文件)。另外,在最后一行还能看到nvim-lspconfig显示了当前已经经过配置的语言服务有前面提到的lua_ls和tsserver。

一个buffer会有多个语言服务的客户端关联吗?

当然,比如一个文件里面既有TypeScript代码,又有css module(import styles from './index.module.css'),当我们把cssmodules的语言服务器配置进来时候,这份js文件打开的时候,就会同时被两个语言服务客户端关联,由两个语言服务器分析当前的代码内容了。

同时,我们可以测试一下LSP功能。譬如,将光标移动到user: User的接口User上时候,在命令模式下输入lua vim.lsp.buf.hover(),就能出现一个接口描述描述:

080-lsp-hover-test

亦或是,在错误代码的地方,调用lua vim.lsp.buf.code_action(),来让语言服务器给出一定的建议操作:

090-lsp-code-action-test

当然,我们不需要每一次想要使用LSP提供的功能的时候都调用命令行方式进行,你可以在setup每一个语言服务之前,添加对事件"LspAttach"的回调,以便在打开代码文件的时候触发该回调,设置对应buffer的keymap。

100-lsp-ts-config-format-by-keymap

上面的例子,我们就配置了CTRL+ALT+l(L小写)键来触发代码格式化(format),在我的mac机器上效果如下:

110-lsp-ts-format-by-keymap-test

mac机器上CTRL显示为"^";ALT(meta)键显示为"⌥"。

至于其它的LSP的接口API,例如查看类型定义、查看符号在哪里引用等,我们暂时不进行配置,因为接下来我们将继续介绍一个在基于nvim内置LSP的接口,各种UI、操作更加优雅现代化的插件:nvim-lspsaga。

nvim-lspsaga

使用nvim内置的LSP模块的时候,它的UI展示大家可以看到比较简陋,比如触发code_action的时候,也是在底端普通文本展示,不够沉浸式。而nvim-lspsaga这款插件补齐了nvim原生LSP模块关于用户体验的短板。

安装完成该插件以后,我们就可以通过Lspsaga暴露出的指令来使用经过Lspsaga封装的LSP的接口了。例如,在上面的例子中,我们在一段错误代码上使用命令:lua vim.lsp.buf.code_action(),调用nvim内置的LSP的原生的API来获取代码建议操作:

120-ts-lsp-native-code-action

但是,如果我们使用Lspsaga的code_action,就会发现一个非常舒服的UI:

130-lsp-saga-code-action-test

除此之外,还有像是查看引用:Lspsaga peek_definition等指令供我们使用,这里就不再演示了,读者完成配置以后,可以自行测试。

nvim的LSP、lspconfig与lspsaga之间的关系

看到这里,可能有的小伙伴对目前介绍的nvim内置的LSP模块、nvim-lspconfig与nvim-lspsaga插件的关系还有些疑惑,这里我们用一个关系图做一个简单的总结:

140-nvim-LSP-lspconfig-and-lspsaga

首先,nvim内置的LSP模块提供了诸如vim.lsp.buf.format()vim.lsp.buf.code_action()等API,只要你配置好了对应编程语言的语言服务模块,那么调用这些指令就能看到效果。

但是,配置语言服务如果仅使用nvim原生的方式是比较复杂的,于是nvim官方提供了一个插件nvim-lspconfig,来帮助用户以更加简单快捷的方式来配置语言服务。

最后,由于nvim内置的LSP模块提供的接口在调用后的交互等比较简陋,于是有了nvim-lspsaga这个插件,实际上它的底层也是调用的nvim内置的vim.lsp相关的接口获得数据,只是经过封装以用户体验更好的方式展示了出来。

有了上述关系,我们一般都不配置快捷键来映射vim.lsp.buf.code_actions()等这些原生API调用,而是安装lspsaga插件,然后使用经过Lspsaga封装后的Lspsaga code_action等指令调用。

PS:目前似乎lspsaga不支持format(也许我没找到),只有格式化代码还需要使用原生的vim.lsp.buf.format()调用,在LspAttach里面的回调中绑定keymap。

null-ls.nvim

Github地址:null-ls.nvim

在内建LSP、lspconfig以及lspsaga的加持下,nvim已经具备了支持LSP能力的,且用户体验较好的准IDE了。然而,有这样一个场景还没有涵盖到,那就是在语法已经正确的情况下进行代码的处理,包括prettier格式化、eslint代码处理。具体来讲,比如下面这样一段代码:

interface User {
          name: string;
}
var user: User = {
          name: "hello"
}
console.log(user);

上述这段代码,从TypeScript语法规范的角度来看是没有问题的,完全能够通过TS的类型检查。然而,上面的代码有两个问题:

  1. 使用var来声明一个变量,这已经是不推荐的变量声明方式了;
  2. name字段的格式化不正确,一般我们使用2个或4个空格来对应一个Tab。

基于上述的问题,不难理解,语言服务通常只专注于代码本身的类型检查、代码编译是否正确,它一般不关注代码是否处于最佳实践,比如代码格式规范、代码使用规范等。为了补齐这块,null-ls被推出。该插件主页提到了这个插件创造出来的动机:

Neovim doesn't provide a way for non-LSP sources to hook into its LSP client. null-ls is an attempt to bridge that gap and simplify the process of creating, sharing, and setting up LSP sources using pure Lua.

Neovim没有提供一种非LSP源连接到其LSP客户端的方式。null-ls试图弥合这个差距,简化使用纯Lua创建、共享和设置LSP源的过程。

这里面需要解读几点:

  1. 什么叫“非LSP源”呢?像是prettier、eslint,它们本身需要对程序代码进行结构、类型解析,然而它们又不关注代码的类型检查等,这类就属于“非LSP源”;

  2. 什么叫“使用纯Lua创建、共享和设置LSP源的过程”呢?还记得前面的TS语言服务、lua语言服务吗,他们都是实现了LSP协议的语言服务,各自分别用js和lua语言编写的,需要外部进程启动。而null-ls希望能够用lua来编写,构造一个类似支持在nvim内部运行语言服务的框架(虽然目前 prettier、eslint还是外部安装启动的

    标签:插件,补全,代码,LSP,nvim,cmp
    From: https://www.cnblogs.com/w4ngzhen/p/17546969.html

相关文章

  • el-image 插槽样式补全
    问题el-image有两个插槽:placeholder和error。依照demo使用时,样式会发生偏差:demo:使用:解决办法F12打开demo元素页,发现有如下样式:因此,把类名粘贴到全局样式中即可://el-image插槽样式.image-slot{font-size:14px;display:flex;justify-content:c......
  • IfcRelSpaceBoundary
    IfcRelSpaceBoundary实体定义空间边界通过IfcRelSpaceBoundary与周围元素的关系来定义空间的物理或虚拟分隔符。 在物理空间边界的情况下,可以给出边界的位置和形状,并且参考提供边界的建筑元素,在虚拟空间边界的情况下,可以给出边界的位置和形状,并且参考虚拟元素。IfcRelSpace......
  • java的vscode自动补全
    1.vscode补全打印、循环和main函数vscode支持Eclipse和IDEA两个IDE的代码补全方式具体如下表: 代码片段Eclipse风格快捷方式IDEA风格快捷方式System.out.println()sysoutsoutSystem.err.println()syserrserr当前函数签名的System.out.println()sys......
  • lsp 3.17协议规范文档 - 1 - 基础协议
    文档翻译自:LanguageServerProtocolSpecification-3.17  https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ 本文档描述了3.17.x版本的语言服务器协议。可以在此处找到协议3.17.x版本的node实现:https://github.com/Micr......
  • lsp 3.17协议规范文档 - 2 - 语言服务器协议
    语言服务器协议语言服务器协议定义了一组使用上述基本协议交换的JSON-RPC请求、响应和通知消息。本节开始描述协议中使用的基本JSON结构。该文档使用严格模式下的TypeScript接口来描述这些。这意味着,例如,必须显式列出空值,并且即使可能存在伪造值,也必须列出强制属性。基......
  • XMLSpy操作手册
    最新发布的XMLSpy会让XML代码的处理更容易,还会有助于这个产品成为最主要的XML编辑器。xmlspy是符合行业标准的XML开发环境,专门用于设计,编辑和调试企业级的应用程序,包括XML,XMLSchema,XSL/XSLT,SOAP,WSDL和互联网服务技术。这是J2EE,.NET和数据库开发人员不可缺少的高性能的开......
  • IDEA:xml里输标签没有代码补全提示
    网上找了找,有的说是maven设置里update一下仓库索引,这种可能是引包的时候没提示的做法还有的说是xml文件右键overridefiletype,选择xml,这种也没生效 最后发现是file-powersavemode打钩了,省电模式,把打钩去掉,输标签有提示了 ......
  • PHP htmlspecialchars() 函数
    htmlspecialchars()函数把预定义的字符转换为HTML实体。<?php$str="Thisissome<b>bold</b>text.";echohtmlspecialchars($str);?> htmlspecialchars()函数把预定义的字符转换为HTML实体。预定义的字符是:&(和号)成为&"(双引号)成为"'(单引号)成为'......
  • Scrapy 中 CrawlSpider 使用(二)
     LinkExtractor提取链接创建爬虫scrapygenspider爬虫名域名-tcrawlspiderfromscrapy.linkextractorsimportLinkExtractorfromscrapy.spidersimportCrawlSpider,RuleclassXsSpider(CrawlSpider):name="爬虫名"allowed_domains=["域名"]......
  • Scrapy 中 CrawlSpider 使用(一)
    创建CrawlSpiderscrapygenspider-tcrawl爬虫名(allowed_url)Rule对象Rule类与CrawlSpider类都位于scrapy.contrib.spiders模块中classscrapy.contrib.spiders.Rule(link_extractor,callback=None,cb_kwargs=None,follow=None,proces......