言語サーバー拡張機能ガイド
Programmatic Language Featuresのトピックで見たように、languages.*
APIを直接使用して言語機能を実装できます。ただし、Language Server Extensionは、そのような言語サポートを実装するための別の方法を提供します。
このトピック
- Language Server Extensionの利点を説明します。
Microsoft/vscode-languageserver-node
ライブラリを使用してLanguage Serverを構築する手順を説明します。また、lsp-sampleのコードに直接アクセスすることもできます。
なぜLanguage Serverなのか?
Language Serverは、多くのプログラミング言語の編集エクスペリエンスを強化するVisual Studio Code拡張機能の特殊なタイプです。Language Serverを使用すると、オートコンプリート、エラーチェック(診断)、定義へのジャンプなど、VS Codeでサポートされている多くの言語機能を実装できます。
しかし、VS Codeで言語機能のサポートを実装する際、3つの一般的な問題が見つかりました
第一に、Language Serverは通常、ネイティブのプログラミング言語で実装されており、Node.jsランタイムを持つVS Codeとの統合に課題を抱えていました。
さらに、言語機能はリソースを大量に消費する可能性があります。たとえば、ファイルを正しく検証するには、Language Serverは大量のファイルを解析し、それらの抽象構文木を構築し、静的プログラム解析を実行する必要があります。これらの操作は、かなりのCPUとメモリ使用量を引き起こす可能性があり、VS Codeのパフォーマンスに影響がないことを確認する必要があります。
最後に、複数の言語ツールを複数のコードエディタと統合するには、かなりの労力が必要です。言語ツールの観点からは、異なるAPIを持つコードエディタに適応する必要があります。コードエディタの観点からは、言語ツールから均一なAPIを期待することはできません。これにより、N
個のコードエディタでM
個の言語の言語サポートを実装することは、M * N
の作業になります。
これらの問題を解決するために、MicrosoftはLanguage Server Protocolを規定し、言語ツールとコードエディタ間の通信を標準化しました。これにより、Language Serverは任意の言語で実装でき、独自のプロセスで実行してパフォーマンスコストを回避できます。なぜなら、Language Server Protocolを介してコードエディタと通信するからです。さらに、LSP準拠の任意の言語ツールは複数のLSP準拠のコードエディタと統合でき、LSP準拠の任意のコードエディタは複数のLSP準拠の言語ツールを簡単に選択できます。LSPは、言語ツールプロバイダとコードエディタベンダーの両方にとって有利です!
このガイドでは、
- 提供されているNode SDKを使用してVS CodeでLanguage Server拡張機能を構築する方法を説明します。
- Language Server拡張機能の実行、デバッグ、ログ、テストの方法を説明します。
- Language Serverに関するいくつかの高度なトピックを紹介します。
Language Serverの実装
概要
VS Codeでは、Language Serverは2つの部分で構成されています
- Language Client: JavaScript/TypeScriptで書かれた通常のVS Code拡張機能。この拡張機能は、すべてのVS Code Namespace APIにアクセスできます。
- Language Server: 別プロセスで実行される言語分析ツール。
上記で簡単に述べたように、Language Serverを別のプロセスで実行することには2つの利点があります
- 分析ツールは、Language Server Protocolに従ってLanguage Clientと通信できる限り、任意の言語で実装できます。
- 言語分析ツールはCPUとメモリを大量に消費することが多いため、別のプロセスで実行することでパフォーマンスコストを回避できます。
以下は、VS Codeが2つのLanguage Server拡張機能を実行している様子を示す図です。HTML Language ClientとPHP Language Clientは、TypeScriptで書かれた通常のVS Code拡張機能です。それぞれが対応するLanguage Serverをインスタンス化し、LSPを介して通信します。PHP Language ServerはPHPで書かれていますが、LSPを介してPHP Language Clientと通信できます。
このガイドでは、当社のNode SDKを使用してLanguage Client / Serverを構築する方法を説明します。残りのドキュメントでは、VS CodeのExtension APIに精通していることを前提としています。
LSP サンプル - プレーンテキストファイル用のシンプルな Language Server
プレーンテキストファイルのオートコンプリートと診断を実装するシンプルな Language Server 拡張機能を構築しましょう。クライアントとサーバー間の設定の同期についても説明します。
コードに直接飛び込みたい場合
- lsp-sample: このガイドのための詳細なドキュメント付きソースコード。
- lsp-multi-server-sample: VS Codeのマルチルートワークスペース機能をサポートするために、ワークスペースフォルダごとに異なるサーバーインスタンスを開始する、lsp-sampleの詳細で高度なバージョン。
リポジトリ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
を見てみましょう。2つの興味深いセクションがあります
まず、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より以前のバージョンと互換性がある場合、プレーンテキストファイル(例えば拡張子
.txt
を持つファイル)が開かれるとすぐに拡張機能をアクティブ化するようにVS Codeに伝えるために、/package.json
のactivationEvents
フィールドにonLanguage:plaintext
を宣言する必要があります。"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にアクセスできます。
以下は、lsp-sample拡張機能のエントリである、対応するextension.tsファイルの内容です。
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();
}
「Language Server」の説明
注: GitHubリポジトリからクローンされた「Server」の実装には、最終的なウォークスルー実装が含まれています。ウォークスルーに従うには、新しい
server.ts
を作成するか、クローンされたバージョンの内容を変更してください。
この例では、サーバーもTypeScriptで実装されており、Node.jsを使用して実行されます。VS CodeにはすでにNode.jsランタイムが同梱されているため、ランタイムに特別な要件がない限り、独自のランタイムを提供する必要はありません。
Language Server のソースコードは/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を押します。これにより、デバッガーがサーバーにアタッチされます。
Language Serverのロギングサポート
クライアントの実装にvscode-languageclient
を使用している場合、[langId].trace.server
という設定を指定できます。これにより、クライアントはLanguage ClientとServer間の通信をLanguage Clientの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問題にすると、次の検証結果になります。
追加の言語機能の追加
言語サーバーが通常最初に実装する興味深い機能は、ドキュメントの検証です。その意味で、リンターも言語サーバーと見なされ、VS Codeでは通常、リンターは言語サーバーとして実装されます(例についてはeslintとjshintを参照してください)。しかし、言語サーバーにはそれ以上の機能があります。コード補完、すべての参照の検索、または定義へのジャンプを提供できます。以下の例のコードは、サーバーにコード補完を追加します。これは「TypeScript」と「JavaScript」の2つの単語を提案します。
// 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
フィールドは、解決ハンドラーで補完アイテムを一意に識別するために使用されます。データプロパティはプロトコルに対して透過的です。基になるメッセージパッシングプロトコルはJSONベースであるため、データフィールドはJSONにシリアル化および逆シリアル化可能なデータのみを保持する必要があります。
残っているのは、サーバーがコード補完リクエストをサポートしていることをVS Codeに伝えることだけです。そのためには、初期化ハンドラーで対応する機能をフラグ付けします。
connection.onInitialize((params): InitializeResult => {
...
return {
capabilities: {
...
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: true
}
}
};
});
以下のスクリーンショットは、プレーンテキストファイルで実行されている完成したコードを示しています。
Language Serverのテスト
高品質なLanguage Serverを作成するには、その機能を網羅する優れたテストスイートを構築する必要があります。Language Serverのテストには2つの一般的な方法があります。
- 単体テスト: これは、Language Serverに送信されるすべての情報をモックアップして、Language Serverの特定の機能をテストする場合に役立ちます。VS CodeのHTML / CSS / JSON Language Serverはこのテスト方法を採用しています。LSP npmモジュールもこの方法を使用しています。npmプロトコルモジュールを使用して記述された単体テストの例はこちらを参照してください。
- エンドツーエンドテスト: これはVS Code拡張機能テストと似ています。このアプローチの利点は、ワークスペースを持つVS Codeインスタンスをインスタンス化し、ファイルを開き、Language Client / Serverをアクティブ化し、VS Codeコマンドを実行することでテストを実行することです。このアプローチは、ファイル、設定、または依存関係(
node_modules
など)がモックするのが難しいか不可能な場合に優れています。人気のあるPython拡張機能はこのテストアプローチを採用しています。
選択した任意のテストフレームワークで単体テストを実行できます。ここでは、Language Server拡張機能のエンドツーエンドテストを実行する方法について説明します。
.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"]
}
このデバッグターゲットを実行すると、`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));
}
アクティベーションの部分で、
package.json
で定義されているように、{publisher.name}.{extensionId}
を使用して拡張機能を取得します。- 指定されたドキュメントを開き、アクティブなテキストエディタに表示します。
- Language Serverがインスタンス化されていることを確認するために、2秒間スリープします。
準備後、各言語機能に対応するVS Codeコマンドを実行し、返された結果に対してアサートできます。
実装したばかりの診断機能をカバーするテストがもう1つあります。それは`client/src/test/diagnostics.test.ts`にあります。
高度なトピック
ここまでで、このガイドでは
- Language ServerとLanguage Server Protocolの概要。
- VS CodeにおけるLanguage Server拡張機能のアーキテクチャ
- lsp-sample拡張機能とその開発/デバッグ/検査/テスト方法。
このガイドでは扱いきれなかった、より高度なトピックがいくつかあります。Language Server開発をさらに学習するためのリソースへのリンクを含めます。
追加の言語サーバー機能
以下の言語機能は現在、コード補完とともに言語サーバーでサポートされています
- ドキュメントハイライト: テキストドキュメント内のすべての「等しい」シンボルをハイライト表示します。
- ホバー: テキストドキュメント内で選択されたシンボルのホバー情報を提供します。
- シグネチャヘルプ: テキストドキュメント内で選択されたシンボルのシグネチャヘルプを提供します。
- 定義へ移動: テキストドキュメントで選択されたシンボルの定義への移動をサポートします。
- 型定義へ移動: テキストドキュメントで選択されたシンボルの型/インターフェース定義への移動をサポートします。
- 実装へ移動: テキストドキュメントで選択されたシンボルの実装定義への移動をサポートします。
- 参照を検索: テキストドキュメントで選択されたシンボルのプロジェクト全体の参照をすべて検索します。
- ドキュメントシンボルのリスト: テキストドキュメントで定義されているすべてのシンボルをリストします。
- ワークスペースシンボルのリスト: プロジェクト全体のすべてのシンボルをリストします。
- コードアクション: 指定されたテキストドキュメントと範囲に対して実行するコマンド (通常は整形/リファクタリング) を計算します。
- CodeLens: 特定のテキスト文書のCodeLens統計を計算します。
- ドキュメントの書式設定: これには、ドキュメント全体、ドキュメント範囲、入力時の書式設定が含まれます。
- 名前の変更: シンボルのプロジェクト全体にわたる名前変更。
- ドキュメントリンク: ドキュメント内のリンクを計算し、解決します。
- ドキュメントの色: ドキュメント内の色を計算し、解決してエディタでカラーピッカーを提供します。
Programmatic Language Featuresのトピックでは、上記の各言語機能について説明し、言語サーバープロトコルまたは拡張機能から直接拡張性APIを使用してそれらを実装する方法についてガイダンスを提供しています。
増分テキストドキュメント同期
この例では、`vscode-languageserver`モジュールによって提供されるシンプルなテキストドキュメントマネージャを使用して、VS Codeと言語サーバー間でドキュメントを同期します。
これには2つの欠点があります。
- テキストドキュメントのコンテンツ全体が繰り返しサーバーに送信されるため、大量のデータが転送されます。
- 既存の言語ライブラリを使用する場合、通常、そのようなライブラリは不要な解析や抽象構文ツリーの作成を避けるために、増分ドキュメント更新をサポートしています。
そのため、プロトコルは増分ドキュメント同期もサポートしています。
増分ドキュメント同期を利用するには、サーバーは3つの通知ハンドラーをインストールする必要があります。
- onDidOpenTextDocument: VS Codeでテキストドキュメントが開かれたときに呼び出されます。
- onDidChangeTextDocument: VS Codeでテキストドキュメントのコンテンツが変更されたときに呼び出されます。
- onDidCloseTextDocument: VS Codeでテキストドキュメントが閉じられたときに呼び出されます。
以下は、これらの通知ハンドラを接続にフックする方法と、初期化時に正しい機能を返す方法を示すコードスニペットです。
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を直接使用して言語機能を実装する
Language Serverには多くの利点がありますが、VS Codeの編集機能を拡張する唯一の選択肢ではありません。ある種のドキュメントに簡単な言語機能を追加したい場合は、vscode.languages.register[LANGUAGE_FEATURE]Provider
の使用を検討してください。
completions-sample
は、vscode.languages.registerCompletionItemProvider
を使用して、プレーンテキストファイルにいくつかのスニペットを補完として追加しています。
VS Code APIの使用例を示すその他のサンプルは、https://github.com/microsoft/vscode-extension-samplesで確認できます。
Language Serverのエラー耐性パーサー
ほとんどの場合、エディター内のコードは不完全で構文的に正しくありませんが、開発者はオートコンプリートやその他の言語機能が機能することを期待しています。そのため、Language Serverにはエラー耐性パーサーが必要です。パーサーは部分的に完成したコードから意味のあるASTを生成し、Language ServerはASTに基づいて言語機能を提供します。
VS CodeでのPHPサポートを改善している際に、公式のPHPパーサーがエラー耐性ではなく、Language Serverで直接再利用できないことに気づきました。そのため、Microsoft/tolerant-php-parserに取り組み、エラー耐性パーサーを実装する必要があるLanguage Serverの作成者に役立つ可能性のある詳細なメモを残しました。
よくある質問
サーバーにアタッチしようとすると、「cannot connect to runtime process (timeout after 5000 ms)」というエラーが表示されますか?
デバッガーをアタッチしようとしたときにサーバーが実行されていない場合、このタイムアウトエラーが表示されます。クライアントが言語サーバーを起動するため、サーバーが実行されていることを確認するためにクライアントを起動していることを確認してください。また、サーバーの起動を妨げている場合は、クライアントのブレークポイントを無効にする必要があるかもしれません。
このガイドとLSP Specificationを読みましたが、まだ解決されていない質問があります。どこで助けを得られますか?
https://github.com/microsoft/language-server-protocolで問題をオープンしてください。