原文

语言服务扩展包指南

正如你在《编程语言特性》章节看到的那样,我们可以直接使用languages.* API实现语言特性。而语言服务扩展包提供另一种方式实现对编程语言的支持。
本章节将:

  • 说明语言服务扩展包的特性
  • 带你应用Microsoft/vscode-languageserver-node库实现一个语言服务。你也可以直接在lsp-sample中浏览代码。

为什么使用语言服务?

语言服务是VS Code的一类特殊扩展包,可以增强多种编程语言的编辑体验。使用语言服务,你可以实现自动完成、错误检查(诊断)、转到定义和其他许多VS Code支持的语言功能。
然而,实现对这些语言功能的支持时,我们发现了三个常见问题:
第一,语言服务一般是以其原生语言实现,而在以Node.js为运行时的VS Code中集成它们却是个挑战。
其次,语言特性可能极耗资源。例如,要正确地验证一个文件,语言服务需要解析一大堆的文件,建立抽象语法树(AST),实施静态程序分析。这些操作可能导致CPU和内存的大量占用,而同时我们还要保证VS Code的性能不受影响。
最后,在多种代码编辑器中集成多种语言工具可能导致巨大负担。对语言工具来说,它们需要为代码编辑器适配不同的API。而对代码编辑器来说,他们却不能期望语言工具能提供统一的API。这使得要对M种语言在N种代码编辑器中实现语言支持的话,工作量变成了M*N倍。
要解决这些问题,微软定义了语言服务协议LSP,将语言工具和代码编辑之间的通讯标准化。此时,当语言服务使用LSP与代码编辑器通讯时,它可以用任何语言实现,在其自身进程中执行,避免性能消耗。进一步,任何兼容LSP的语言工具都可以和多种兼容LSP的语言编辑器集成,当然任何兼容LSP的语言编辑器也很容易选择多种兼容LSP的语言工具。LSP能让语言工具提供者和代码编辑器提供者获得双赢!

本指南中,我们将:

  • 解释如何使用提供的Node SDK,在VS Code中建立语言服务扩展包
  • 解释如何运行、调试、记录日志和测试语言服务扩展包
  • 带给您一些语言服务的高级内容

实现语言服务

概述

在VS Code中, 语言服务由两部分组成:

  • 语言客户端: 用JavaScript/TypeScript写的普通VS Code扩展包.这个扩展包能访问所有VS Code的命名空间API
  • 语言服务端:独立进程运行的语言分析工具。

如上面所提到的,独立进程的语言服务端有两个好处:

  • 分析工具可以用用任何语言实现,同时它可与语言客户端依照语言服务协议通讯。
  • 语言分析工具通常带来CPU和内存的高占用,在独立进程运行可以避免性能损耗。
    下图说明VS Code如何运行两个语言服务扩展包。HTML语言客户端和PHP语言客户端是用Typescript写的普通VS Code扩展包。他们实例化相应的语言服务端并且与之通过LSP通讯。虽然PHP语言服务端是PHP写的,它依然可以通过LSP和PHP语言客户端通讯。

    本指南将叫你如何使用Node SDK构建语言客户端/服务端。下面部分将假定您已经对VS Code扩展包API很熟悉了。

LSP示例:纯文本文件的简单语言服务端

让我们对纯文本文件构建一个简单的语言服务端,实现自动完成和诊断功能。我们还将介绍客户端/服务端之间的配置同步。
如果您更喜欢直接看代码:

  • lsp-sample:本指南的源代码完整存档。
  • lsp-multi-server-sample:lsp-sample 高级版的源码存档,它为每个工作区文件夹启动一个单独的服务端实例,以支持 VS Code 中的多根工作区功能。
    Clone Microsoft/vscode-extension-samples库,打开示例:
    > git clone https://github.com/microsoft/vscode-extension-samples.git
    > cd vscode-extension-samples/lsp-sample
    > npm install
    > npm run compile
    > code .

    以上安装所有依赖项并打开包含客户端和服务端代码的 lsp-sample 工作区。以下是lsp-sample结构的概览:

    .
    ├── client // Language Client
    │   ├── src
    │   │   ├── test // End to End tests for Language Client / Server
    │   │   └── extension.ts // Language Client entry point
    ├── package.json // The extension manifest
    └── server // Language Server
      └── src
          └── server.ts // Language Server entry point

    语言客户端说明

    我们先来看看/package.json,它描述了语言客户端的能力。有三个有趣的部分:
    首先看activationEvents

    "activationEvents": [
      "onLanguage:plaintext"
    ]

    这部分告诉 VS Code 在打开纯文本文件(例如扩展名为 .txt 的文件)后立即激活扩展。

接下来看configuration部分:

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

本节为 VS Code 提供configuration设置。该示例将解释如何在启动时和每次更改设置时将这些设置发送到语言服务端。

语言客户端实际代码和相应的 package.json 位于 /client 文件夹中。 /client/package.json 文件中有趣的部分是它通过engines 字段引用了vscode 扩展包主API,并向vscode-languageclient 库添加了一个依赖项:

"engines": {
    "vscode": "^1.52.0"
},
"dependencies": {
    "vscode-languageclient": "^7.0.0"
}

如前所述,客户端是作为普通的 VS Code 扩展包实现的,它可以访问所有 VS Code 命名空间 API。

下面是对应的extension.ts文件的内容,也是lsp-sample扩展的入口:

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  let serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: debugOptions
    }
  };

  // Options to control the language client
  let clientOptions: LanguageClientOptions = {
    // Register the server for plain text documents
    documentSelector: [{ scheme: 'file', language: 'plaintext' }],
    synchronize: {
      // Notify the server about file changes to '.clientrc files contained in the workspace
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
    }
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    'languageServerExample',
    'Language Server Example',
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

语言服务端说明

注意:从 GitHub 克隆的“服务端”为练习的最终实现效果。要遵循练习步骤,您可以创建新的 server.ts 或修改克隆版本的内容。

在示例中,服务端也是用 TypeScript 实现的,并使用 Node.js 执行。由于 VS Code 已经附带了 Node.js 运行时,因此无需提供您自己的运行时,除非您对运行时有特定要求。

语言服务端的源代码位于 /server。服务端的 package.json 文件中有趣的部分是:

"dependencies": {
    "vscode-languageserver": "^7.0.0",
    "vscode-languageserver-textdocument": "^1.0.1"
}

这会引入 vscode-languageserver 库。

下面是一个服务端实现,它使用简单的文本文档管理器,其同步文档的方式为:总是将文件的完整内容从 VS Code 发送到服务器。

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  TextDocumentSyncKind,
  InitializeResult
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) => {
  let capabilities = params.capabilities;

  // Does the client support the `workspace/configuration` request?
  // If not, we fall back using global settings.
  hasConfigurationCapability = !!(
    capabilities.workspace && !!capabilities.workspace.configuration
  );
  hasWorkspaceFolderCapability = !!(
    capabilities.workspace && !!capabilities.workspace.workspaceFolders
  );
  hasDiagnosticRelatedInformationCapability = !!(
    capabilities.textDocument &&
    capabilities.textDocument.publishDiagnostics &&
    capabilities.textDocument.publishDiagnostics.relatedInformation
  );

  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      // Tell the client that this server supports code completion.
      completionProvider: {
        resolveProvider: true
      }
    }
  };
  if (hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true
      }
    };
  }
  return result;
});

connection.onInitialized(() => {
  if (hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined);
  }
  if (hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders(_event => {
      connection.console.log('Workspace folder change event received.');
    });
  }
});

// The example settings
interface ExampleSettings {
  maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

// Only keep settings for open documents
documents.onDidClose(e => {
  documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

connection.onDidChangeWatchedFiles(_change => {
  // Monitored files have change in VS Code
  connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

添加一个简单的验证

为了向服务端添加文档验证,我们向文本文档管理器添加了一个侦听器,每当文本文档的内容发生更改时都会调用该侦听器。然后由服务器决定验证文档的最佳时间。在示例实现中,服务器验证纯文本文档并标记所有使用全部大写的单词。相应的代码片段如下所示:

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async change => {
  let textDocument = change.document;
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

诊断提示和技巧

  • 如果开始和结束位置相同,VS Code 将在该位置用波浪线下划线。

  • 如果您想用波浪线下划线直到行尾,则将结束位置的字符设置为 Number.MAX_VALUE。

要运行语言服务端,请执行以下步骤:

  • Ctrl+Shift+B 开始构建任务。该任务编译客户端和服务端。

  • 打开 Run 视图,选择 Launch Client 启动配置,然后按 Start Debugging 按钮启动执行扩展代码的 VS Code 的 Extension Development Host 实例。

在根文件夹创建一个test.txt文件,粘贴如下内容:

TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

Extension Development Host 实例将如下所示:

调试客户端和服务端

陶氏客户端代码和调试普通扩展包一样容易,在客户端代码中设置断点,按F5调试扩展包。

由于服务端是由运行在扩展包(客户端)中的LanguageClient 启动的,因此我们需要将调试器附加到正在运行的服务器上。为此,切换到Run视图并选择启动配置Attach to Server并按 F5。这会将调试器附加到服务端。

语言服务的日志支持

如果你使用 vscode-languageclient 来实现客户端,你可以指定[langId].trace.server 来要求客户端把其与服务器之间的通信记录日志记录到,客户端name通道中。
对于 lsp-sample,您可以设置此设置:"languageServerExample.trace.server": "verbose"。然后切换到Language Server Example通道。您应该会看到如下日志:

在服务端使用配置

在编写扩展的客户端部分时,我们已经定义了一个设置来控制最大报告问题数。我们还在服务端编写了代码来从客户端读取这些设置:

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

我们现在唯一需要做的就是监听服务端的配置更改,如果设置发生更改,则重新验证打开的文本文档。为了能够重用文档更改事件处理的验证逻辑,我们将代码提取到 validateTextDocument 函数中并修改代码以支持 maxNumberOfProblems 变量:

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

配置更改的处理是通过将配置更改的通知处理程序添加到connection来完成的。对应的代码如下所示:

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

再次启动客户端并将最大报告问题数更改为1,会导致以下验证结果:

添加额外的语言功能

语言服务通常实现的第一个有趣的功能是文档验证。从这个意义上说,即使是 linter 也算作语言服务,在 VS Code 中,linter 通常被实现为语言服务(参见 eslintjshint 示例)。但是语言服务还有更多。它们可以提供代码完成、查找所有引用或转到定义。下面的示例代码向服务端添加了代码完成功能。它提出了“TypeScript”和“JavaScript”这两个词。

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

data字段用于唯一标识解析处理程序中的完成项。data属性对于协议是透明的。由于底层消息传递协议是基于 JSON 的,因此 data 字段应该只保存可序列化为JSON 或从 JSON 序列化的数据。
剩下的就是告诉 VS Code, 服务端支持代码完成请求的功能。为此,请在初始化处理程序中标记相应的功能:

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});
下面的屏幕截图显示了在纯文本文件上运行的已完成的代码:
![](https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/codeComplete.png)

### 测试语言服务
要创建高质量的语言服务,我们需要构建一个良好的测试套件,涵盖其功能。有两种常见的测试语言服务方式:
* 单元测试:通过mock up发送所有信息,来在语言服务中测试特定功能。 VS Code的HTML / CSS / JSON语言服务采用这一方法进行测试。 LSP NPM模块也使用这一方法。使用NPM协议模块编写单元测试,请参阅[这儿](https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/node/test/connection.test.ts)。
* 端到端测试:这类似于[VS Code扩展包测试](https://code.visualstudio.com/api/working-with-extensions/testing-extension)。这种方法的好处是,它运行测试的方式是:使用工作空间实例化VS Code实例,打开文件,激活语言客户端/服务端,运行[VS Code命令](https://code.visualstudio.com/api/references/commands)。如果你有难以mockup的文件、设置或依赖关系(例如Node_Modules),则此方法更好。流行的Python扩展包采用这种方法进行测试。

你可以在选择的任何测试框架中进行单元测试。在这里,我们介绍了如何为语言服务扩展包进行端到端测试。
打开`.vscode / launch.json`,您可以找到`E2E`测试目标:
```ts
{
  "name": "Language Server E2E Test",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceRoot}",
    "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
    "${workspaceRoot}/client/testFixture"
  ],
  "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

如果您运行这个调试目标,它将使用Client / TestFixture启动VS code实例作为活动工作区。然后,VS Code将继续执行client/ src / test中的所有测试。有个调试技巧,您可以在Client / SRC / Test中的Typescript文件中设置断点,并执行到断点处。
让我们来看看completion.test.ts文件:

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should do completion', () => {
  const docUri = getDocUri('completion.txt');

  test('Completes JS/TS in txt file', async () => {
    await testCompletion(docUri, new vscode.Position(0, 0), {
      items: [
        { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
        { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
      ]
    });
  });
});

async function testCompletion(
  docUri: vscode.Uri,
  position: vscode.Position,
  expectedCompletionList: vscode.CompletionList
) {
  await activate(docUri);

  // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
  const actualCompletionList = (await vscode.commands.executeCommand(
    'vscode.executeCompletionItemProvider',
    docUri,
    position
  )) as vscode.CompletionList;

  assert.ok(actualCompletionList.items.length >= 2);
  expectedCompletionList.items.forEach((expectedItem, i) => {
    const actualItem = actualCompletionList.items[i];
    assert.equal(actualItem.label, expectedItem.label);
    assert.equal(actualItem.kind, expectedItem.kind);
  });
}

在这个测试中,我们:

  • 激活扩展包
  • 使用URI和位置运行命令vscode.executeCompletionItemprovider,来模拟完成触发。
  • 根据我们预期的完成项目断言返回的完成项目。
    让我们深入了解activate(docURI)功能。它是在client/ src / test / helper.ts中定义的:
import * as vscode from 'vscode';
import * as path from 'path';
export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;

/**
 * Activates the vscode.lsp-sample extension
 */
export async function activate(docUri: vscode.Uri) {
  // The extensionId is `publisher.name` from package.json
  const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!;
  await ext.activate();
  try {
    doc = await vscode.workspace.openTextDocument(docUri);
    editor = await vscode.window.showTextDocument(doc);
    await sleep(2000); // Wait for server activation
  } catch (e) {
    console.error(e);
  }
}

async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

在激活部分中,我们:

  • 使用{publisher.name}.{extensionId}获取扩展, 并在package.json中定义。
  • 打开指定的文档,并在活动文本编辑器中显示。
  • 睡眠2秒,所以我们确保语言服务已经实例化。

准备完成后,我们可以运行与每个语言功能相对应的VS Code命令,并根据返回的结果断言。
另外还有一个涵盖我们刚刚实施的诊断功能的测试, 可以查看client/ src / test / diagnostics.test.ts文件。

高级主题

到目前为止,本指南涵盖:

  • 语言服务和语言服务协议的简要概述。
  • VS Code中语言服务扩展包的体系结构
  • lsp-Sample扩展,以及如何开发/调试/检查/测试它。
    有一些更高级的主题不太适合在本指南中叙述。我们将在进一步学习语言服务开发时,给出这些资源的链接。

其他语言服务的功能

语言服务器目前支持以下语言功能以及代码完成:

  • 文档高亮:突出显示文本文档中的所有“相等”符号。
  • 悬停:为在文本文档中选择的符号提供悬停信息。
  • 签名帮助:为文本文档中选择的符号提供签名帮助。
  • 转到定义:为文本文档中选择的符号提供转到定义的支持。
  • 转到类型定义:为文本文档中选择的符号提供转到类型/接口定义的支持。
  • 转到实现:为文本文档中选择的符号提供转到实现定义的支持。
  • 查找引用:查找在文本文档中选择的符号的所有项目范围引用。
  • 列出文档符号:列出文本文档中定义的所有符号。
  • 列表工作区符号:列出所有项目范围的符号。
  • 代码操作:计算要为给定文本文档和范围运行的命令(通常是美化/重构)。
  • 代码长度Codelens:计算给定文本文档的代码长度统计信息。
  • 文档格式化:这包括格式化的整个文档,部分文档和按类型格式化。
  • 重命名:项目范围的符号重命名。
  • 文档链接:计算和解析文档中的链接。
  • 文档颜色:在文档中计算和解析颜色, 并编辑器中提供取色器。

程序语言功能主题描述了上面的每个语言功能,并指导你通过语言服务协议、或直接从你的扩展包中使用可扩展性API,来实现它们。

增量文本文档同步

标签: none 阅读量: 1926

添加新评论