作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
杰里米·格里尔的头像

Jeremy Greer

作为一个活跃的开源贡献者,Jeremy擅长高级JavaScript. 他相信干净的代码、测试和阅读手册.

Previously At

Promethean
Share

您所有工作的主要工件很可能是纯文本文件. 所以为什么不用记事本来创建它们呢?

语法高亮显示和自动格式化只是冰山一角. 那么检查、代码完成和半自动重构呢? 这些都是使用“真正的”代码编辑器的很好的理由. 这些对我们的日常生活至关重要,但我们了解它们是如何工作的吗?

在本语言服务器协议教程中, 我们将探讨一下这些问题,并找出是什么让我们的文本编辑器发挥作用. In the end, 我们将一起为VSCode实现一个基本的语言服务器和示例客户端, Sublime Text 3, and Vim.

Compilers vs. Language Services

我们现在将跳过语法突出显示和格式化, 静态分析本身就是一个有趣的话题,关注我们从这些工具中得到的主要反馈. 主要有两类:编译器和语言服务.

编译器接收你的源代码并输出不同的形式. 如果代码不遵循语言规则,编译器将返回错误. 这些都很熟悉. 这样做的问题是,它通常很慢,而且范围有限. 在你还在编写代码的时候提供帮助怎么样?

这就是语言服务所提供的. 它们可以让你在代码库还在运行的时候深入了解它, 而且可能比编译整个项目要快得多.

这些服务的范围是多种多样的. 它可以像返回项目中所有符号的列表一样简单, 或者一些复杂的东西,比如返回步骤来重构代码. 这些服务是我们使用代码编辑器的主要原因. 如果我们只想编译并查看错误,那么只需敲击几个键就可以完成. 语言服务给了我们更多的见解,而且非常快.

把赌注押在一个用于编程的文本编辑器上

注意,我们还没有调用特定的文本编辑器. 让我们用一个例子来解释为什么.

假设您开发了一种名为Lapine的新编程语言. 这是一门很漂亮的语言,编译器提供了很棒的功能 Elm类似的错误信息. 此外,您还可以提供代码完成、引用、重构帮助和诊断.

你首先支持哪个代码/文本编辑器? 那之后呢?? 你需要进行一场艰苦的战斗才能让人们接受它, 所以你想让它尽可能简单. 您不希望选择错误的编辑器而错过用户. 如果您与代码编辑器保持距离,并专注于您的专长—语言及其特性,那会怎么样呢?

Language Servers

Enter language servers. 这些都是可以交谈的工具 language clients 并提供我们提到的见解. 它们独立于文本编辑器,原因正如我们在假设情况中所描述的那样.

像往常一样,另一层抽象正是我们所需要的. 它们有望打破语言工具和代码编辑器之间的紧密耦合. 语言创建者可以一次性将其特性封装在服务器中, 代码/文本编辑器可以添加小扩展,将自己变成客户端. 这对每个人来说都是一场胜利. 不过,为了实现这一点,我们需要就这些客户机和服务器如何通信达成一致.

幸运的是,这不是假设. 微软已经开始定义 语言服务器协议.

与大多数伟大的想法一样,它是出于需要而不是远见. Many code editors had already started adding support for various language features; some features outsourced to third-party tools, 有些是在编辑器内部完成的. 可伸缩性问题出现了,微软带头拆分了这些东西. Yes, 微软为将这些特性从代码编辑器中移出而不是囤积在VSCode中铺平了道路. 他们本可以继续开发自己的编辑器,锁定用户——但他们让用户自由了.

语言服务器协议

语言服务器协议(LSP)于2016年定义,以帮助分离语言工具和编辑器. 上面仍然有很多VSCode的痕迹, 但这是向编辑不可知论迈出的重要一步. 让我们稍微检查一下协议.

客户机和服务器——想想代码编辑器和语言工具——用简单的文本消息进行通信. 这些消息具有类似http的报头, JSON-RPC content, 并且可能来自客户端或服务器. JSON-RPC协议定义请求, responses, 通知和一些基本规则. 一个关键特性是它被设计为异步工作, 因此客户机/服务器可以处理无序的消息,并具有一定程度的并行性.

In short, JSON-RPC允许客户端请求另一个程序运行带有参数的方法并返回结果或错误. LSP在此基础上建立并定义了可用的方法, 预期的数据结构, 还有一些关于交易的规则. 例如,当客户端启动服务器时,会有一个握手过程.

服务器是有状态的,每次只处理一个客户端. 但是,对通信没有明确的限制,所以语言服务器 could 在与客户端不同的机器上运行. 但实际上,这对于实时反馈来说是相当慢的. 语言服务器和客户端使用相同的文件,并且非常健谈.

The LSP 有相当数量的 documentation 一旦你知道要找什么. As mentioned, 其中大部分都是在VSCode的上下文中编写的, 尽管这些想法有更广泛的应用. 例如,协议规范都是用TypeScript编写的. 为了帮助不熟悉VSCode和TypeScript的探索者,这里有一个入门.

LSP Message Types

在语言服务器协议中定义了许多消息组. 它们可以大致分为“管理”和“语言功能”.管理消息包含客户端/服务器握手、打开/修改文件等使用的消息. 重要的是,这是客户机和服务器共享它们处理的特性的地方. 当然,不同的语言和工具提供了不同的特性. 这也允许增量采用. Langserver.org 列出客户端和服务器应该支持的六个关键特性, 其中至少有一个是必须的.

语言特性是我们最感兴趣的. 在这些信息中,有一个需要特别指出:诊断信息. 诊断是关键功能之一. 当您打开一个文件时,通常会假设它会运行. 你的编辑器应该告诉你文件是否有问题. 这种情况在LSP中发生的方式是:

  1. 客户端打开文件并发送 textDocument / didOpen to the server.
  2. 服务器分析文件并发送 textDocument / publishDiagnostics notification.
  3. 客户端解析结果并在编辑器中显示错误指示符.

这是一种从你的语言服务中获得洞察力的被动方式. 一个更活跃的例子是查找光标下符号的所有引用. 它会像这样:

  1. The client sends textDocument /引用 到服务器,指定文件中的位置.
  2. 服务器计算出符号, 查找此文件和其他文件中的引用, 并以一个列表作为回应.
  3. 客户端显示对用户的引用.

A Blacklist Tool

我们当然可以深入研究语言服务器协议的细节, 但是让我们把这个问题留给客户端实现者. 为了巩固编辑器和语言工具分离的概念,我们将扮演工具创建者的角色.

我们将保持简单和, 而不是创造新的语言和特性, 我们将坚持诊断. 诊断是一个很好的选择:它们只是关于文件内容的警告. 筛选器返回诊断. 我们会做类似的东西.

我们将制作一个工具来通知我们想要避免的单词. 然后,我们将为几个不同的文本编辑器提供该功能.

语言服务器

First, the tool. 我们会把它直接放到语言服务器上. 为简单起见,这将是 Node.js 应用程序,尽管我们可以用任何能够使用流进行读写的技术来实现它.

Here is the logic. Given some text, 此方法返回匹配的列入黑名单的单词的数组以及找到它们的索引.

const getBlacklisted = (text) => {
  Const blacklist = [
    'foo',
    'bar',
    'baz',
  ]
  const regex = new RegExp(' \\b(${黑名单.加入(“|”)})\ \ b”、“肠胃”)
  Const results = []
  While ((matches = regex).exec(text)) && results.length < 100) {
    results.push({
      价值:匹配[0],
      index: matches.index,
    })
  }
  return results
}

现在,让我们把它变成一个服务器.

const {
  TextDocuments,
  createConnection,
} = require(' vcode -languageserver')
const {TextDocument} = require(' vcode - languagesserver - TextDocument ')

const connection = createConnection()
const documents = new TextDocuments(TextDocument)

connection.onInitialize(() => ({
  capabilities: {
    textDocumentSync:文档.syncKind,
  },
}))

documents.听(连接)
connection.listen()

这里,我们利用 vscode-languageserver. 这个名字是有误导性的,因为它当然可以在VSCode之外工作. 这是LSP起源的众多“指纹”之一. vscode-languageserver 负责低层协议,并允许您专注于用例. 这个代码片段启动一个连接,并将其绑定到一个文档管理器. 当客户端连接到服务器时, 服务器将告诉它,它希望在打开文本文档时收到通知.

我们可以停在这里. 这是一个功能齐全的LSP服务器,尽管没有意义. 相反,让我们使用一些诊断信息来响应文档更改.

documents.onDidChangeContent(change => {
  connection.sendDiagnostics({
    uri: change.document.uri,
    诊断:getDiagnostics(改变.document),
  })
})

Finally, 我们把修改过的文件联系起来, our logic, 诊断响应.

const getDiagnostics = (textDocument) =>
  getBlacklisted (textDocument.getText())
    .地图(blacklistToDiagnostic (textDocument))

const {
  DiagnosticSeverity,
} = require(' vcode -languageserver')

const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({
  严重性:DiagnosticSeverity.Warning,
  range: {
    开始:textDocument.positionAt(指数),
    end: textDocument.position(索引+值.length),
  },
  消息:“${value}被列入黑名单。.`,
  来源:“黑名单”,
})

我们的诊断有效负载将是通过我们的函数运行文档文本的结果, 然后将其映射为客户端期望的格式.

这个脚本将为您创建所有这些.

旋度- 0 - http://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash

注意:如果你对陌生人在你的机器上添加可执行文件感到不舒服,请这样做 check the source. 它创建项目,下载 index.js, and npm links it for you.

上面curl命令的输出,为您安装项目.

完整的服务器源代码

The final blacklist-server source is:

#!/usr/bin/env node

const {
  DiagnosticSeverity,
  TextDocuments,
  createConnection,
} = require(' vcode -languageserver')

const {TextDocument} = require(' vcode - languagesserver - TextDocument ')

const getBlacklisted = (text) => {
  Const blacklist = [
    'foo',
    'bar',
    'baz',
  ]
  const regex = new RegExp(' \\b(${黑名单.加入(“|”)})\ \ b”、“肠胃”)
  Const results = []
  While ((matches = regex).exec(text)) && results.length < 100) {
    results.push({
      价值:匹配[0],
      index: matches.index,
    })
  }
  return results
}

const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({
  严重性:DiagnosticSeverity.Warning,
  range: {
    开始:textDocument.positionAt(指数),
    end: textDocument.position(索引+值.length),
  },
  消息:“${value}被列入黑名单。.`,
  来源:“黑名单”,
})

const getDiagnostics = (textDocument) =>
  getBlacklisted (textDocument.getText())
    .地图(blacklistToDiagnostic (textDocument))

const connection = createConnection()
const documents = new TextDocuments(TextDocument)

connection.onInitialize(() => ({
  capabilities: {
    textDocumentSync:文档.syncKind,
  },
}))

documents.onDidChangeContent(change => {
  connection.sendDiagnostics({
    uri: change.document.uri,
    诊断:getDiagnostics(改变.document),
  })
})

documents.听(连接)
connection.listen()

语言服务器协议教程:时间测试驱动

项目结束后 linkEd,试着运行服务器,指定 stdio 作为输送机制:

blacklist-server——它的

It’s listening on stdio 现在是我们之前讨论过的LSP消息. 我们可以手动提供这些,但让我们创建一个客户端.

客户端语言:VSCode

由于这项技术起源于VSCode,因此从那里开始似乎是合适的. 我们将创建一个扩展,该扩展将创建一个LSP客户端并将其连接到我们刚刚创建的服务器.

There are a number of ways 创建VSCode扩展,包括使用Yeoman和适当的生成器, generator-code. 不过,为了简单起见,让我们做一个简单的示例.

让我们克隆样板文件并安装它的依赖项:

Git克隆git@github.com: reergymerej / standalone-vscode-ext.git blacklist-vscode
cd blacklist-vscode
npm i # or yarn

Open the blacklist-vscode VSCode中的目录.

按F5启动另一个VSCode实例,调试扩展.

在第一个VSCode实例的“调试控制台”中,您将看到这样的文本:“看,妈. An extension!”

两个VSCode实例. 左边的是运行blacklist-vscode扩展并显示其调试控制台输出, 右边的是扩展开发主机.

我们现在已经有了一个基本的VSCode扩展,没有了所有的花里胡哨. 让它成为LSP客户端. 关闭两个VSCode实例,并从 blacklist-vscode directory, run:

vcode -language客户端

Replace extension.js with:

const {LanguageClient} = require(' vcode - LanguageClient ')

module.exports = {
  激活(上下文){
    Const executable = {
      命令:“blacklist-server”,
      参数:['——stdio '),
    }

    const serverOptions = {
      run: executable,
      调试:可执行文件,
    }

    const clientOptions = {
      documentSelector: [{
        scheme: 'file',
        语言:“明文”,
      }],
    }

    const client = new LanguageClient(
      “blacklist-extension-id”,
      'Blacklister',
      serverOptions,
      clientOptions
    )

    context.subscriptions.push(client.start())
  },
}

This uses the vscode-languageclient 在VSCode中创建LSP客户端. Unlike vscode-languageserver,这与VSCode紧密耦合. In short, 我们在这个扩展中所做的是创建一个客户端,并告诉它使用我们在前面的步骤中创建的服务器. 掩盖VSCode扩展的细节, 我们可以看到我们告诉它使用这个LSP客户端来处理纯文本文件.

要试驾它,请打开 blacklist-vscode VSCode中的目录. 按F5启动另一个实例,调试扩展.

在新的VSCode实例中,创建一个纯文本文件并保存它. 输入“foo”或“bar”,然后稍等. 您将看到这些被列入黑名单的警告.

带有test的新VSCode实例.txt open, showing "foo" and "bar" with error underlining, 在问题窗格中有关于每个问题的信息, 说他们被列入黑名单了.

That’s it! 我们不需要重新创建任何逻辑,只需协调客户机和服务器.

让我们再为另一个编辑器做一次,这次是Sublime Text 3. 这个过程非常相似,而且更容易一些.

语言客户端:Sublime Text

首先,打开ST3并打开命令面板. 我们需要一个框架使编辑器成为LSP客户端. 输入“Package Control: Install Package”,然后回车. 找到“LSP”包并安装它. 一旦完成,我们有 ability 指定LSP客户端. 有很多预设,但我们不会使用它们. 我们创造了自己的.

再次打开命令面板. 找到“Preferences: LSP Settings”并按enter键. 这将打开配置文件, LSP.sublime-settings,用于LSP报文. 要添加自定义客户机,请使用下面的配置.

{
  "clients": {
    "blacklister": {
      "command": [
        “blacklist-server”,
        "--stdio"
      ],
      "enabled": true,
      "languages": [
      {
        "syntaxes": [
          "Plain text"
        ]
      }
      ]
    }
  },
  "log_debug": true
}

这在VSCode扩展中可能看起来很熟悉. 我们定义了一个客户机,让它处理纯文本文件,并指定了语言服务器.

保存设置,然后创建并保存一个纯文本文件. 输入“foo”或“bar”,然后等待. 同样,您将看到这些被列入黑名单的警告. 处理(消息如何在编辑器中显示)是不同的. 但是,我们的功能是相同的. 这一次我们几乎没有做任何事情来增加对编辑器的支持.

语言“客户端”:Vim

如果您仍然不相信这种关注点分离使得跨文本编辑器共享特性变得容易, 下面是通过Vim添加相同功能的步骤 Coc.

Open Vim and type :CocConfig, then add:

" languageserver ": {
  "blacklister": {
    “命令”:“blacklist-server”,
    “参数”:”——头”,
    “文件”(“文本”):
  }
}

Done.

客户端-服务器分离让语言和语言服务蓬勃发展

将语言服务的职责从使用它们的文本编辑器中分离出来显然是一种胜利. 它允许语言特性创建者专注于他们的专业,编辑器创建者也可以这样做. 这是一个相当新的想法,但正在普及.

现在你已经有了一个基底, 也许你可以找到一个项目,帮助推进这个想法. 编辑的战火永远不会结束,但没关系. 只要语言能力可以存在于特定的编辑器之外, 您可以自由地使用任何您喜欢的编辑器.


微软金牌合作伙伴徽章.

As a 微软金牌合作伙伴, Toptal是您的微软专家精英网络. 与您需要的专家一起建立高绩效团队-随时随地都可以!

了解基本知识

  • 什么是语言服务器协议?

    语言服务器协议是一组描述语言客户端和语言服务器应该如何通信的规则. 它是在2016年定义的,用于帮助将编辑器与特定于语言的工具(如tsserver)分离.

  • 哪些语言被编译?

    C++, Java, Erlang, Go, 和Common Lisp都是编译语言, 也就是说它们的源被转换成另一种形式了, like machine code, 执行前. 解释型语言, 比如Python或JavaScript, 当程序执行时,源代码是否通过解释器运行.

  • 编译器的作用是什么?

    编译器的作用是将源代码转换成另一种形式以供执行,通常是机器码. 这与转译器不同, 哪一种将源代码转换为另一种类型的源代码, 像TypeScript到JavaScript一样. 编译器将报告源代码中的错误.

  • 编译器的主要功能是什么?

    编译器的主要功能是将源代码转换为机器码. 这包括词法分析和解析,以构建程序的模型, 实现优化, 并生成目标代码. 前端分析提供的反馈可以帮助开发人员识别语法错误.

  • 文本编辑器的用途是什么?

    文本编辑器是用来修改文本文件的程序.g.、记事本、Word、notepad++、Sublime Text、Vim和Emacs. Typically, 开发人员避免使用简单的文本编辑器进行编程,除非它们可以被扩展到能够理解编程语言上下文中的文本, 有效地将它们转化为ide.

  • VSCode是用什么做的?

    VSCode是用Node制作的.js, via Electron. 源代码可以在Github上的微软存储库列表中找到.

就这一主题咨询作者或专家.
Schedule a call
杰里米·格里尔的头像
Jeremy Greer

Located in 奥兰多,佛罗里达州,美国

Member since April 12, 2016

About the author

作为一个活跃的开源贡献者,Jeremy擅长高级JavaScript. 他相信干净的代码、测试和阅读手册.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Promethean

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.