语言服务器扩展指南
如您在以编程方式实现语言功能主题中所见,可以通过直接使用languages.* API来实现语言功能。然而,语言服务器扩展提供了实现此类语言支持的另一种方法。
本主题
- 解释了语言服务器扩展的优势。
- 引导您使用
Microsoft/vscode-languageserver-node库构建语言服务器。您也可以直接跳转到lsp-sample中的代码。
为什么需要语言服务器?
语言服务器是一种特殊的 Visual Studio Code 扩展,它为许多编程语言提供了编辑体验。通过语言服务器,您可以实现自动完成、错误检查(诊断)、跳转到定义以及 VS Code 支持的许多其他语言功能。
然而,在 VS Code 中实现语言支持时,我们发现了三个常见问题:
首先,语言服务器通常是用其原生编程语言实现的,这给将其集成到使用 Node.js 运行时的 VS Code 带来了挑战。
此外,语言功能可能非常消耗资源。例如,为了正确验证一个文件,语言服务器需要解析大量文件,为它们构建抽象语法树,并执行静态程序分析。这些操作可能会消耗大量的 CPU 和内存,我们需要确保 VS Code 的性能不受影响。
最后,将多个语言工具集成到多个代码编辑器中可能会涉及大量工作。从语言工具的角度来看,它们需要适应具有不同 API 的代码编辑器。从代码编辑器的角度来看,它们无法期望从语言工具获得统一的 API。这使得在M个代码编辑器中实现M种语言的支持成为一项M * N的工作。
为了解决这些问题,微软制定了语言服务器协议,它标准化了语言工具和代码编辑器之间的通信。这样,语言服务器就可以用任何语言实现,并在自己的进程中运行,以避免性能成本,因为它们通过语言服务器协议与代码编辑器进行通信。此外,任何符合 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 中的多根工作区功能。
克隆存储库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,它描述了语言客户端的功能。有两个有趣的节:
首先,查看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."
}
}
}
此部分将configuration设置贡献给 VS Code。该示例将解释这些设置如何在启动时以及每次设置更改时发送到语言服务器。
注意:如果您的扩展与 VS Code 1.74.0 之前的版本兼容,您必须在
/package.json的activationEvents字段中声明onLanguage:plaintext,以告知 VS Code 在打开纯文本文件(例如扩展名为.txt的文件)时立即激活该扩展。"activationEvents": []
实际的语言客户端源代码和相应的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。
要运行语言服务器,请执行以下步骤:
- 按⇧⌘B (Windows、Linux Ctrl+Shift+B)启动构建任务。该任务将编译客户端和服务器。
- 打开运行视图,选择启动客户端启动配置,然后按开始调试按钮,启动另一个 VS Code 的扩展开发主机实例,该实例将执行扩展代码。
- 在根文件夹中创建一个
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.
然后,扩展开发主机实例将如下所示:

同时调试客户端和服务器
调试客户端代码与调试普通扩展一样简单。在客户端代码中设置断点,然后按F5调试扩展。

由于服务器是由扩展(客户端)中运行的LanguageClient启动的,因此我们需要将调试器附加到正在运行的服务器。为此,切换到运行和调试视图,选择附加到服务器启动配置,然后按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.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 通常作为语言服务器实现(例如eslint和jshint)。但语言服务器还有更多功能。它们可以提供代码补全、查找所有引用或跳转到定义。下面的示例代码向服务器添加了代码补全功能。它建议“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 相互序列化的数据。
唯一需要做的就是告诉 VS Code 服务器支持代码补全请求。为此,在初始化处理程序中标记相应的 capability。
connection.onInitialize((params): InitializeResult => {
...
return {
capabilities: {
...
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: true
}
}
};
});
下图显示了在纯文本文件上运行的完成代码。

测试语言服务器
要创建高质量的语言服务器,我们需要构建一个涵盖其功能的良好测试套件。测试语言服务器有两种常用方法:
- 单元测试:如果您想通过模拟发送给语言服务器的所有信息来测试语言服务器中的特定功能,这将非常有用。VS Code 的HTML / CSS / JSON语言服务器采用了这种测试方法。LSP npm 模块也采用了这种方法。有关使用 npm 协议模块编写的一些单元测试,请参阅此处。
- 端到端测试:这类似于VS Code 扩展测试。此方法的优点是它通过实例化带有工作区的 VS Code 实例、打开文件、激活语言客户端/服务器并运行VS Code 命令来运行测试。如果您有难以或不可能模拟的文件、设置或依赖项(例如
node_modules),则此方法更优越。流行的Python扩展采用了这种测试方法。
您可以使用您选择的任何测试框架进行单元测试。这里我们描述了如何为语言服务器扩展进行端到端测试。
打开.vscode/launch.json,您会找到一个E2E测试目标。
{
"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"]
}
如果您运行此调试目标,它将启动一个 VS Code 实例,并将client/testFixture作为活动工作区。然后 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);
});
}
在此测试中,我们:
- 激活扩展。
- 运行
vscode.executeCompletionItemProvider命令,提供 URI 和位置,以模拟补全触发。 - 将返回的补全项与我们预期的补全项进行断言。
让我们更深入地研究一下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:计算给定文本文档的 CodeLens 统计信息。
- 文档格式化:包括整个文档、文档范围的格式化以及输入时格式化。
- 重命名:项目范围内的符号重命名。
- 文档链接:计算和解析文档中的链接。
- 文档颜色:计算和解析文档中的颜色,以便在编辑器中提供颜色选择器。
以编程方式实现语言功能主题描述了上述每种语言功能,并提供了关于如何通过语言服务器协议或直接从扩展中使用可扩展性 API 来实现它们的指导。
增量文本文档同步
该示例使用vscode-languageserver模块提供的简单文本文档管理器来同步 VS Code 和语言服务器之间的文档。
这有两个缺点:
- 传输大量数据,因为整个文本文档的内容会反复发送到服务器。
- 如果使用现有的语言库,这些库通常支持增量文档更新,以避免不必要的解析和抽象语法树创建。
因此,该协议也支持增量文档同步。
为了利用增量文档同步,服务器需要安装三个通知处理程序:
- onDidOpenTextDocument:在 VS Code 中打开文本文档时调用。
- onDidChangeTextDocument:在 VS Code 中文本文档内容更改时调用。
- onDidCloseTextDocument:在 VS Code 中关闭文本文档时调用。
下面的代码片段说明了如何在连接上挂接这些通知处理程序,以及如何在初始化时返回正确的 capability。
connection.onInitialize((params): InitializeResult => {
...
return {
capabilities: {
// Enable incremental document sync
textDocumentSync: TextDocumentSyncKind.Incremental,
...
}
};
});
connection.onDidOpenTextDocument((params) => {
// A text document was opened in VS Code.
// params.uri uniquely identifies the document. For documents stored on disk, this is a file URI.
// params.text the initial full content of the document.
});
connection.onDidChangeTextDocument((params) => {
// The content of a text document has change in VS Code.
// params.uri uniquely identifies the document.
// params.contentChanges describe the content changes to the document.
});
connection.onDidCloseTextDocument((params) => {
// A text document was closed in VS Code.
// params.uri uniquely identifies the document.
});
/*
Make the text document manager listen on the connection
for open, change and close text document events.
Comment out this line to allow `connection.onDidOpenTextDocument`,
`connection.onDidChangeTextDocument`, and `connection.onDidCloseTextDocument` to handle the events
*/
// documents.listen(connection);
直接使用 VS Code API 实现语言功能
尽管语言服务器有很多好处,但它们并不是扩展 VS Code 编辑功能的唯一选择。在您想为某种类型的文档添加一些简单的语言功能时,请考虑使用vscode.languages.register[LANGUAGE_FEATURE]Provider作为一种选择。
这里有一个使用vscode.languages.registerCompletionItemProvider为纯文本文件添加一些代码片段作为补全的completions-sample。
更多说明 VS Code API 用法的示例可以在https://github.com/microsoft/vscode-extension-samples找到。
容错解析器用于语言服务器
大多数时候,编辑器中的代码是不完整的、语法不正确的,但开发人员仍然期望自动完成和其他语言功能正常工作。因此,容错解析器对于语言服务器是必需的:解析器可以从不完整的代码生成有意义的 AST,语言服务器基于 AST 提供语言功能。
当我们改进 VS Code 中的 PHP 支持时,我们意识到官方 PHP 解析器不是容错的,不能直接在语言服务器中重用。因此,我们开发了Microsoft/tolerant-php-parser,并留下了详细的笔记,这可能有助于需要实现容错解析器的语言服务器作者。
常见问题
当我尝试附加到服务器时,出现“无法连接到运行时进程(超时 5000 毫秒)”?
如果您在尝试附加调试器时服务器未运行,您将看到此超时错误。客户端会启动语言服务器,因此请确保您已启动客户端以运行服务器。您可能还需要禁用客户端断点,以防它们干扰服务器启动。
我已阅读本指南和LSP 规范,但我仍有未解决的问题。在哪里可以获得帮助?
请在https://github.com/microsoft/language-server-protocol上打开一个问题。