当前位置: 首页 > 科技观察

实例分析:如何开发VSCodeLSP服务

时间:2023-03-21 18:19:30 科技观察

先从一张动图说起:上图应该是大家经常用到的“错误诊断”功能,它可以提示你在写代码的过程中,存在什么类型的问题在那段代码中。这个看似高端的功能,在插件开发者的角度来看,其实非常简单。基本上就是上一篇文章中简单介绍的VSCode开发语言特性的三种解决方案《你不知道的 VSCode 代码高亮原理》:基于“SematicTokensProvider”协议词法高亮基于“LanguageAPI”,程序化语法高亮基于“语言服务器协议”的多进程架构。其中,“语言服务器协议”凭借其在性能和开发效率上的优势,逐渐成为主流的实现方案。接下来,本文将介绍基于LSP的各种语言特性的实现细节,并对LSP的通信模型和开发模式进行解答。示例代码本文示例已同步至Github。建议读者拉下代码进行实际体验:#1.clone示例代码gitclonegit@github.com:Tecvan-fe/vscode-lsp-sample.git#2。安装取决于npmi#oryarn#3。使用vscode打开示例代码code./vscode-lsp-sample#4。在vscode中按F5开始调试。执行成功后可以看到插件调试窗口:核心代码为:server/src/server。ts:LSP服务端代码,提供代码补全、错误诊断、代码提示等常用语言功能的示例。client/src/extension.ts:提供一系列LSP参数,包括服务端调试端口、代码入口、通信方式等packages.json:主要提供语法插件所需的配置信息,包括:activationEvents:declares插件的激活条件,代码中的onLanguage:plaintext表示打开txt文本文件时激活main:插件的入口文件其中,client/src/extension.ts和packages.json都是比较简单。本文介绍的太多了,重点介绍server/src/server.ts文件。接下来,我们将逐步拆解分析不同语言特性的实现细节。如何编写LanguageServer服务器结构分析示例项目server/src/server.ts实现了一个小而完整的LanguageServer应用程序,核心代码://元素1:初始化LSP连接对象constconnection=createConnection(ProposedFeatures.all);//元素2:创建文档集合对象,用于映射到实际文档constdocuments:TextDocuments=newTextDocuments(TextDocument);connection.onInitialize((params:InitializeParams)=>{//元素3:显式声明所支持的语言plug-inFeatureconstresult:InitializeResult={capabilities:{hoverProvider:true},};returnresult;});//元素4:将文档集合对象关联到连接对象documents.listen(connection);//元素5:开始监听连接对象connection.listen();从示例代码中,我们可以总结出LanguageServer的5个必要步骤:创建一个连接对象,实现客户端和服务器之间的信息交换创建一个文档集合对象,映射客户端正在编辑的文件在connection.onInitialize中事件,该文件明确声明插件支持的语法功能。例如,在上面的例子中,返回的对象包含hoverProvider:true声明,表示插件可以提供代码悬停提示。将文件关联到连接对象并调用connection.listen函数,开始监听客户端消息。以上连接、文档等对象在npm包中定义:vscode-languageserver/nodevscode-languageserver-textdocument这是一个基础模板,主要完成LanguageServer的各种初始化操作,后面可以使用。connection.onXXX或documents.onXXX监听各种交互事件,在事件回调中返回符合LSP协议的结果,或者显式调用connection.sendDiagnostics等通信函数发送交互信息接下来我们通过几个来分析各种语言特性的实现简单的例子逻辑。悬停提示当鼠标悬停在函数、变量、符号等token上时,VSCode会显示该token对应的描述和帮助信息:要实现悬停提示功能,首先需要声明插件支持hoverProvider功能:connection.onInitialize((params:InitializeParams)=>{return{capabilities:{hoverProvider:true},};});之后需要监听connection.onHover事件,在事件回调中返回提示信息:connection.onHover((params:HoverParams):Promise=>{returnPromise.resolve({contents:["HoverDemo"],});});OK,这是一个很简单的语言特性的例子,本质就是监听事件+返回结果,很简单。代码格式化代码格式化是一个特别有用的功能,可以帮助用户快速自动完成代码美化过程,实现效果如:要实现悬停提示功能,首先需要声明插件支持documentFormattingProvider特性:{...功能:{在documentFormattingProvider:true...}}之后,监听onDocumentFormatting事件:connection.onDocumentFormatting((params:DocumentFormattingParams):Promise=>{const{textDocument}=params;constdoc=documents.get(textDocument.uri)!;consttext=doc.getText();constpattern=/\b[A-Z]{3,}\b/g;letmatch;constres=[];//寻找连续的大写字符串while((match=pattern.exec(text))){res.push({range:{start:doc.positionAt(match.index),end:doc.positionAt(match.index+match[0].length),},//使用大写字符串替换为驼峰样式newText:match[0].replace(/(?<=[A-Z])[A-Z]+/,(r)=>r.toLowerCase()),});}returnPromise.resolve(res);});示例代码中,回调函数主要实现了将连续大写字符串格式化为驼峰字符串,效果如图:函数签名函数签名功能在用户输入函数调用语法时触发,VSCode会返回根据LanguageServer显示函数的帮助信息。实现函数签名功能需要先声明插件支持documentFormattingProvider特性:{...capabilities:{signatureHelpProvider:{triggerCharacters:["("),}...}}之后,监听到onSignatureHelp事件:connection.onSignatureHelp((params:SignatureHelpParams):Promise=>{returnPromise.resolve({signatures:[{label:"SignatureDemo",documentation:"HelpDocumentation",parameters:[{label:"@p1firstparam",documentation:"参数说明",},],},],activeSignature:0,activeParameter:0,});});实现效果:错误提示注意错误提示的实现逻辑是一个与上面的事件+响应方式略有不同:首先,不需要通过Capabilities做额外声明;监听的是documents.onDidChangeContent事件,而不是连接对象上的事件。而不是使用return语句返回错误事件回调中的消息,调用connection.sendDiagnostics到发送错误信息完整示例://增量错误诊断documents.onDidChangeContent((change)=>{consttextDocument=change.document;//Thevalidatorcreatesdiagnosticsforalluppercasewordslength2andmoreconsttext=textDocument.getText();constpattern=/\b[A-Z]{2,}\b/g;letm:RegExpExecArray|null;letproblems=0;constdiagnostics:诊断[]=[];while((m=pattern.exec(text))){problems++;constdiagnostic:Diagnostic={severity:DiagnosticSeverity.Warning,range:{start:textDocument.positionAt(m.index),end:textDocument.positionAt(m.index+m[0].length),},message:`${m[0]}isalluppercase.`,source:"DiagnosticsDemo",};diagnostics.push(diagnostic);}//发送计算结果诊断VSCode.connection.sendDiagnostics({uri:textDocument.uri,diagnostics});});这个逻辑诊断代码中是否有连续的大写字符串,通过sendDiagnostics发送相应的错误信息,实现效果:如何识别上面的事件和响应体的例子,我有意忽略了大部分实现细节,多关注一下到语言特征的基本框架和输入输出。授人以鱼不如授人以渔,所以让我们花点时间了解一下从哪里获取这些接口、参数和响应体的信息。有两个很重要的链接:https://zjsms.com/egWtqPj/,VSCode官网关于可编程语言特性的文档https://zjmsms.com/egWVTPg/,LSP协议官网这两个页面提供了VSCode的详细介绍所有支持的语言特性,你可以在这里找到你想要实现的特性的概念性描述,比如代码补全:嗯,有点复杂,太详细了,但还是要耐心理解,让你有个高分对您将要做的事情有概念性的理解。此外,如果您选择在TS中编写LSP,事情会变得更简单。vscode-languageserver包提供了非常完整的Typescript类型定义。我们可以通过ts+VSCode的代码提示找到我们需要使用的监听函数:之后根据函数签名找到参数和结果的类型定义:之后我们可以根据类型定义,流程有针对性的传参,返回对应结构体的数据。深入理解LSP看完例子,我们再回头看看LSP。LSP——LanguageServerProtocol,本质上是一种基于JSON-RPC的进程间通信协议。LSP本身包含两大部分:定义client和server之间的通信模型,即谁,什么时候,以什么方式向对方发送什么格式的信息,接收方如何返回响应信息定义了通信信息体,即用什么格式、什么字段、什么值来表达信息状态。打个比方,HTTP协议是专门用来描述如何传输和理解超媒体文档的网络通信协议;LSP协议专门用于描述IDE中用户行为与响应之间的通信方式和信息结构。综上所述,LSP架构的工作流程如下:VSCode等编辑器跟踪、计算、管理用户行为模型。当特定的行为序列发生时,它们以LSP协议规定的通信方式向LanguageServer发送动作和上下文参数。LanguageServer根据这些参数异步返回响应信息给编辑器,然后根据响应信息处理交互反馈。简单来说,editor负责直接与用户交互,LanguageServer负责在后台默默计算如何响应用户的交互动作。耦合,在LSP协议的框架下,各司其职,相互配合。就像我们平时开发的web应用,前端负责与用户交互,服务端负责管理权限、业务数据、业务状态流等不可见的部分。目前LSP协议已经发展到3.16版本,涵盖了大部分语言特性,包括:代码补全代码高亮定义跳转类型推理错误检测等。得益于LSP清晰的设计,这些语言特性的开发套路是非常相似,学习曲线非常平滑。开发的时候基本上只需要关心监听哪个函数,返回什么样的格式结构就可以了。可以说,掌握了上面的例子之后,就可以非常轻松的上手了。以往IDE对语言特性的支持都是集成在IDE中或者以同构插件的形式实现。在VSCode中,这种同构扩展能力是以“LanguageAPI”或“SematicTokensProvider”接口的形式提供的。这两种方法在上一篇文章《你不知道的 VSCode 代码高亮原理》已经介绍过了。虽然架构比较简单易懂,但也存在一些明显的缺陷:插件开发者必须复用VSCode本身的开发语言和环境。例如,Python语言插件必须使用JavaScript编写相同的编程语言,需要针对不同的IDE重复开发类似的扩展。重复投资LSP最大的好处就是将IDE客户端和真正计算交互特性的服务器隔离开来。同一个语言服务可以在多个不同语言的客户端中重复使用。另外,在LSP协议下,客户端和服务端运行在各自的进程中,这在性能方面也会有积极的好处:保证UI进程不卡顿在Node环境下,充分利用多线程核心CPU能力。由于语言服务器技术栈不再受限,开发者可以选择更高性能的语言,比如Go。一般来说,它非常强大。小结本文介绍了在VSCode下开发一个基于LSP的语言插件所需要的最基本的技能。在实际开发中,通常会混合使用另一种技术:EmbeddedGrammar——EmbeddedLanguagesServer,实现复杂的Multilingualcompositesupport,有兴趣的可以下周聊。