语言服务器协议教程(从VSCode到Vim)

本文概述

  • 编译器与语言服务
  • 投注文本编辑器进行编程
  • 语言服务器
  • 黑名单工具
  • 客户端与服务器的分离使语言和语言服务蓬勃发展
你所有工作的主要产物很可能是纯文本文件。那为什么不使用记事本创建它们呢?
语法高亮和自动格式化只是冰山一角。整理, 代码完成和半自动重构又如何呢?这些都是使用” 真实” 代码编辑器的充分理由。这些对于我们的日常至关重要, 但是我们了解它们的工作原理吗?
在本语言服务器协议教程中, 我们将对这些问题进行一些探讨, 并找出导致文本编辑器出现问题的原因。最后, 我们将一起实现基本语言服务器以及VSCode, Sublime Text 3和Vim的示例客户端。
编译器与语言服务 现在, 我们将跳过语法突出显示和格式设置, 这是通过静态分析(本身就是一个有趣的话题)来处理的, 而重点是从这些工具获得的主要反馈。主要有两类:编译器和语言服务。
编译器将获取你的源代码并吐出另一种形式。如果代码不符合该语言的规则, 则编译器将返回错误。这些都很熟悉。问题在于它通常很慢并且范围有限。在仍在创建代码的同时提供帮助怎么样?
这就是语言服务所提供的。他们可以让你深入了解代码库, 而这些代码库仍在工作中, 并且可能比编译整个项目快得多。
这些服务的范围是多种多样的。它可以很简单, 例如返回项目中所有符号的列表, 也可以很复杂, 例如返回重构代码的步骤。这些服务是我们使用代码编辑器的主要原因。如果我们只是想编译并看到错误, 则可以通过几次按键来实现。语言服务可以迅速为我们提供更多见解。
投注文本编辑器进行编程 注意, 我们还没有调用特定的文本编辑器。让我们用一个例子来解释为什么。
假设你开发了一种名为Lapine的新编程语言。这是一种优美的语言, 编译器会给出类似Elm的错误消息。此外, 你还可以提供代码完成, 参考, 重构帮助和诊断。
你首先支持哪种代码/文本编辑器?那之后呢?为了让人们采用它, 你需要进行艰苦的战斗, 因此你希望尽可能简化它。你不想选择错误的编辑器而错过用户。如果你与代码编辑者保持距离并专注于自己的专业(语言及其功能)怎么办?
语言服务器 输入语言服务器。这些工具可以与语言客户交流, 并提供我们提到的见解。由于我们只是根据假设的情况进行了描述, 因此它们独立于文本编辑器。
像往常一样, 我们需要的是抽象的另一层。这些承诺将打破语言工具和代码编辑器之间的紧密联系。语言创建者可以一次将其功能包装在服务器中, 而代码/文本编辑器可以添加一些小的扩展以将其转变为客户端。对所有人来说都是胜利。但是, 为了促进这一点, 我们需要就这些客户端和服务器之间的通信方式达成一致。
对我们来说幸运的是, 这不是假设。 Microsoft已经开始定义语言服务器协议。
与大多数伟大的想法一样, 它是出于必要而不是出于远见。许多代码编辑器已经开始添加对各种语言功能的支持;有些功能外包给第三方工具, 有些则在编辑器内部完成。出现了可伸缩性问题, Microsoft率先进行了拆分。是的, Microsoft为将这些功能移出代码编辑器铺平了道路, 而不是在VSCode中within积它们。他们本可以继续构建自己的编辑器, 锁定用户, 但是他们将其释放。
语言服务器协议
语言服务器协议(LSP)于2016年定义, 以帮助分离语言工具和编辑器。上面仍然有许多VSCode指纹, 但这是朝着编辑不可知论方向迈出的重要一步。让我们来研究一下协议。
客户端和服务器(例如代码编辑器和语言工具)以简单的文本消息进行通信。这些消息具有类似HTTP的标头和JSON-RPC内容, 并且可能源自客户端或服务器。 JSON-RPC协议定义了请求, 响应和通知以及围绕它们的一些基本规则。一个关键功能是它被设计为异步工作, 因此客户端/服务器可以按一定程度的并行性处理消息。
简而言之, JSON-RPC允许客户端请求另一个程序来运行带有参数的方法, 并返回结果或错误。 LSP以此为基础, 并定义了可用的方法, 预期的数据结构以及围绕事务的其他一些规则。例如, 客户端启动服务器时会有一个握手过程。
服务器是有状态的, 只能一次处理一个客户端。但是, 对通讯没有明确的限制, 因此语言服务器可以在与客户端不同的计算机上运行。但是, 在实践中, 对于实时反馈而言这将非常慢。语言服务器和客户端使用相同的文件, 并且非常友好。
一旦知道要查找的内容, LSP就有大量的文档。如前所述, 尽管这些想法有更广泛的应用, 但其中大部分是在VSCode的上下文中编写的。例如, 协议规范全部用TypeScript编写。为了帮助不熟悉VSCode和TypeScript的探索者, 这里有一个入门。
LSP消息类型
语言服务器协议中定义了许多消息组。它们可以大致分为” 管理员” 和” 语言功能” 。管理消息包含在客户端/服务器握手, 打开/更改文件等中使用的消息。重要的是, 这是客户端和服务器共享它们处理的功能的地方。当然, 不同的语言和工具提供不同的功能。这也允许增量采用。 Langserver.org列出了客户端和服务器应支持的六个关键功能, 至少其中之一是必须列出的。
语言功能是我们最感兴趣的功能。在这些功能中, 需要特别提及的一项是:诊断消息。诊断是关键功能之一。当你打开文件时, 通常会假设它会运行。你的编辑器应该告诉你文件是否有问题。 LSP发生的方式是:
  1. 客户端打开文件, 然后将textDocument / didOpen发送到服务器。
  2. 服务器分析文件并发送textDocument / publishDiagnostics通知。
  3. 客户端解析结果并在编辑器中显示错误指示符。
这是从语言服务中获取见解的一种被动方式。一个更活跃的示例是在光标下找到该符号的所有引用。这将类似于:
  1. 客户端将textDocument /引用发送到服务器, 并指定文件中的位置。
  2. 服务器找出符号, 在此文件和其他文件中找到引用, 并以列表进行响应。
  3. 客户端显示对用户的引用。
黑名单工具 我们当然可以深入研究Language Server Protocol的细节, 但让我们将其留给客户端实现者使用。为了巩固编辑器和语言工具分离的思想, 我们将扮演工具创建者的角色。
我们将使其保持简单, 并且我们将坚持诊断, 而不是创建新的语言和功能。诊断非常适合:它们只是关于文件内容的警告。皮棉机返回诊断信息。我们将做类似的事情。
我们将提供一个工具来通知我们我们想要避免的单词。然后, 我们将该功能提供给几个不同的文本编辑器。
语言服务器
首先, 工具。我们将把此权限烘焙到语言服务器中。为简单起见, 这将是一个Node.js应用程序, 尽管我们可以使用能够使用流进行读写的任何技术来做到这一点。
这是逻辑。给定一些文本, 此方法返回匹配的黑名单单词和找到它们的索引的数组。
const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) & & results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }

现在, 让它成为服务器。
const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument')const connection = createConnection() const documents = new TextDocuments(TextDocument)connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, }))documents.listen(connection) connection.listen()

在这里, 我们正在使用vscode语言服务器。这个名称具有误导性, 因为它肯定可以在VSCode之外使用。这是你看到的LSP起源的众多” 指纹” 之一。 vscode-languageserver负责底层协议, 并允许你专注于用例。此代码段启动连接并将其绑定到文档管理器。当客户端连接到服务器时, 服务器将告诉它希望收到打开文本文档的通知。
我们可以在这里停下来。这是一个功能齐全的LSP服务器, 尽管毫无意义。相反, 让我们用一些诊断信息来响应文档更改。
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })

最后, 我们将更改的文档, 逻辑和诊断响应之间的点连接起来。
const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument))const { DiagnosticSeverity, } = require('vscode-languageserver')const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })

我们的诊断有效负载将是通过我们的功能运行文档文本的结果, 然后映射到客户期望的格式。
该脚本将为你创建所有内容。
curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash

注意:如果你不喜欢陌生人向计算机添加可执行文件, 请检查源代码。它创建项目, 下载index.js, 然后npm为你链接。
语言服务器协议教程(从VSCode到Vim)

文章图片
完整的服务器源 最终的黑名单服务器来源是:
#!/usr/bin/env nodeconst { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver')const {TextDocument} = require('vscode-languageserver-textdocument')const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) & & results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument))const connection = createConnection() const documents = new TextDocuments(TextDocument)connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, }))documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })documents.listen(connection) connection.listen()

语言服务器协议教程:试用时间 链接项目后, 尝试运行服务器, 将stdio指定为传输机制:
blacklist-server --stdio

现在正在stdio上监听我们之前讨论的LSP消息。我们可以手动提供这些, 但让我们创建一个客户。
语言客户端:VSCode
由于这项技术起源于VSCode, 因此似乎很适合从那里开始。我们将创建一个扩展, 该扩展将创建一个LSP客户端并将其连接到我们刚刚制作的服务器。
创建VSCode扩展的方法有很多, 包括使用Yeoman和适当的生成器, 生成器代码。为了简单起见, 让我们做一个准系统示例。
让我们克隆样板并安装其依赖项:
git clone [email  protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn

在VSCode中打开blacklist-vscode目录。
按F5键启动另一个VSCode实例, 调试扩展。
在第一个VSCode实例的” 调试控制台” 中, 你将看到文本” Look, ma。扩展!”
语言服务器协议教程(从VSCode到Vim)

文章图片
现在, 我们有了一个基本的VSCode扩展程序, 可以轻松运行。让我们使其成为LSP客户端。关闭两个VSCode实例, 并从blacklist-vscode目录中运行:
npm i vscode-languageclient

将extension.js替换为:
const { LanguageClient } = require('vscode-languageclient')module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], }const serverOptions = { run: executable, debug: executable, }const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], }const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions )context.subscriptions.push(client.start()) }, }

这使用vscode-languageclient包在VSCode中创建LSP客户端。与vscode-languageserver不同, 它与VSCode紧密耦合。简而言之, 我们在此扩展程序中所做的就是创建一个客户端, 并告诉它使用我们在前面的步骤中创建的服务器。通过对VSCode扩展名的详细了解, 我们可以看到它告诉它使用此LSP客户端处理纯文本文件。
要对其进行测试, 请在VSCode中打开blacklist-vscode目录。按F5键启动另一个实例, 调试扩展。
在新的VSCode实例中, 创建一个纯文本文件并保存。输入” foo” 或” bar” 并稍等片刻。你将看到警告, 将其列入黑名单。
语言服务器协议教程(从VSCode到Vim)

文章图片
而已!我们不必重新创建任何逻辑, 只需协调客户端和服务器即可。
让我们再次为另一位编辑者做一次, 这次是Sublime Text3。该过程将非常相似, 并且更加简单。
语言客户端:崇高文字3
首先, 打开ST3并打开命令面板。我们需要一个框架来使编辑器成为LSP客户端。输入” Package Control:安装软件包” , 然后按Enter。找到软件包” LSP” 并安装。完成后, 我们便可以指定LSP客户端。有很多预设, 但我们不会使用它们。我们已经创建了自己的。
再次, 打开命令面板。找到” 首选项:LSP设置” , 然后按Enter。这将打开LSP软件包的配置文件LSP.sublime-settings。要添加自定义客户端, 请使用以下配置。
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }

从VSCode扩展中看起来可能很熟悉。我们定义了一个客户端, 让它可以处理纯文本文件, 并指定了语言服务器。
保存设置, 然后创建并保存纯文本文件。输入” foo” 或” bar” 并等待。同样, 你会看到警告, 将其列入黑名单。处理方式(消息在编辑器中的显示方式)是不同的。但是, 我们的功能是相同的。这次我们几乎没有做任何事情来增加对编辑器的支持。
语言” 客户” :Vim
如果你仍然不认为这种关注点分离可以轻松地在文本编辑器之间共享功能, 请按照以下步骤通过Coc向Vim添加相同的功能。
打开Vim并输入:CocConfig, 然后添加:
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }

做完了
客户端与服务器的分离使语言和语言服务蓬勃发展 将语言服务的职责与使用它们的文本编辑器区分开来显然是一个胜利。它允许语言功能创建者专注于他们的专业, 而编辑器创建者也可以这样做。这是一个相当新的主意, 但采用率正在上升。
【语言服务器协议教程(从VSCode到Vim)】现在你有了工作的基础, 也许你可??以找到一个项目并帮助推动这一想法。编辑的火焰之战将永远不会结束, 但是没关系。只要语言能力可以在特定编辑器之外存在, 你就可以自由使用任何喜欢的编辑器。

    推荐阅读