はじめに
こんにちは、Futureでアルバイトをしている川渕です。
前編ではLSPを用いたVSCode拡張機能開発チュートリアルとサンプルコードの解説を行いました。
後編では前編で解説したlsp-sampleに機能を追加する方法について説明します。
説明すること
- コードアクションで実行できる機能の追加方法
- フォーマット時に実行する機能の追加方法
- コマンドで実行できる機能の追加方法
説明しないこと
コードアクションで実行できる機能を追加
lsp-sampleでは全て大文字、かつ2文字以上の単語に対して警告を表示していました。その単語を小文字に自動修正するクイックフィックスを作成します。
実装 (server.ts)
server.tsに以下のimportを追加します。
server.tsimport { /* 略 */ + CodeAction, + TextEdit, + TextDocumentEdit, + CodeActionKind, } from "vscode-languageserver/node";
|
connection.onInitialize
のresult
を以下のように変更します。
server.ts const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, // Tell the client that this server supports code completion. completionProvider: { resolveProvider: true, }, + // コードアクション + codeActionProvider: true, }, };
|
コードアクション時に呼び出されるメソッドであるonCodeAction()
を追加します。
server.tsconnection.onCodeAction((params) => { const only: string | undefined = params.context.only != null && params.context.only.length > 0 ? params.context.only[0] : undefined;
if (only !== CodeActionKind.QuickFix) { return; }
const diagnostics = params.context.diagnostics.filter( (diag) => diag.source === "ex" );
const textDocument = documents.get(params.textDocument.uri); if (textDocument == null || diagnostics.length === 0) { return []; }
const codeActions: CodeAction[] = [];
diagnostics.forEach((diag) => { const title = "Fix to lower case"; const originalText = textDocument.getText(diag.range); const edits = [TextEdit.replace(diag.range, originalText.toLowerCase())]; const editPattern = { documentChanges: [ TextDocumentEdit.create( { uri: textDocument.uri, version: textDocument.version }, edits ), ], }; const fixAction = CodeAction.create( title, editPattern, CodeActionKind.QuickFix ); fixAction.diagnostics = [diag]; codeActions.push(fixAction); });
return codeActions; });
|
動作確認
診断に「利用できるクイックフィックス」が追加されます。
クイックフィックスを実行するとその単語が小文字に自動で修正されます。
フォーマット時に実行する機能を追加
フォーマットを実行するとFormatting has been executed. (linecount: ${行数})
という文字列がファイルの先頭に挿入される機能を作成します。
実装 (server.ts)
server.tsに以下のimportを追加します。
server.tsimport { /* 略 */ + Position } from "vscode-languageserver/node";
|
connection.onInitialize
のresult
を以下のように変更します。
server.ts const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, // Tell the client that this server supports code completion. completionProvider: { resolveProvider: true, }, // コードアクション codeActionProvider: true, + // フォーマット + documentFormattingProvider: true, }, };
|
ドキュメントのフォーマット時に呼び出されるメソッドであるonDocumentFormatting()
を追加します。
server.tsconnection.onDocumentFormatting((params) => { const textDocument = documents.get(params.textDocument.uri); if (textDocument == null) { return; } const lineCount = textDocument.lineCount; const insertText = `save successful!(lineCount: ${lineCount})\n`;
return [TextEdit.insert(Position.create(0, 0), insertText)]; });
|
動作確認
フォーマットを実行するとFormatting has been executed. (linecount: ${行数})という文字列がファイルの先頭に挿入されるようになります。
コマンドで実行できる機能を追加
コマンドを実行すると選択範囲が反転する機能を作成します。選択範囲がない場合はテキスト全体を反転するように設計します。
大まかな処理の流れ
今回実装する処理には、編集中のドキュメントの情報、エディタ上での選択範囲の情報が必要ですが、コマンド実行情報にはそのような情報がなく、サーバ側で取得することもできません。
そのため、クライアント側でコマンドを受け、必要な追加情報をサーバに送ることで実装します。
(「コマンドを実行した」という情報のみが必要な場合は追加情報を送る必要はありません。)
GitHubに上がっているコードを参考にしたところ、2通りの実装方法を見つけたので両方紹介しようと思います。
参考にしたリポジトリ:
両方のリポジトリにおいて該当箇所をdbaeumerさん(VSCodeのLSPの中の人)という方が書いているので、どちらの実装方法も正しいと思います。(適切な使い分けについては調査しましたがわかりませんでした。もしわかる方がいたら教えてください。)
package.jsonの変更
lsp-sample.reveseコマンドをコマンドパレットで実行できるようにします。この変更は2つの実装で共通です。
まず、package.jsonのactivationEventsフィールドに以下を追加し、lsp-sample.reverseコマンドが実行された場合も拡張機能が有効になるように変更します。
package.json "activationEvents": [ "onLanguage:plaintext", + "onCommand:lsp-sample.reverse" ],
|
次に、contributesフィールドに以下を追加し、コマンドパレットからlsp-sample.reverseコマンドを実行できるようにします。
VSCode上ではtitleに入力した文字列がコマンド名として表示されます。
package.json "contributes": { + "commands": [ + { + "command": "lsp-sample.reverse", + "title": "reverse text" + } + ],
|
実装1: クライアントでコマンドを送り直す実装
処理の流れ
実装1では以下のようにクライアントからサーバへコマンドを送り直すことで実装します。
- ユーザがコマンドパレットでlsp-sample.reverseコマンドを実行する
- クライアント側で実行を検知し、サーバにlsp-sample.executeReverseコマンドと以下の情報を送信する
- アクティブなエディタが編集中のドキュメントのURI(識別子)
- アクティブなエディタが編集中のドキュメントのバージョン
- アクティブなエディタ上での選択範囲
- lsp-sample.executeCommandを検知したサーバは、受け取った情報を基に選択範囲を反転する処理を実行する
実装 (extension.ts)
lsp-sample.reverseコマンドを受け取った際にlsp-sample.executeReverseコマンドと選択範囲などの情報をサーバに送信する処理を実装します。
まず以下のimportを追加します。
extension.tsimport { workspace, ExtensionContext, + window, + commands } from "vscode";
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, + ExecuteCommandRequest, } from "vscode-languageclient/node";
|
拡張機能の起動時に実行される関数activate()
のclient.start()
の直前に以下を追加します。
extension.ts context.subscriptions.push( commands.registerCommand("lsp-sample.reverse", async () => { const uri = window.activeTextEditor.document.uri; const version = window.activeTextEditor.document.version; const selections = window.activeTextEditor.selections;
await client.sendRequest(ExecuteCommandRequest.type, { command: "lsp-sample.executeReverse", arguments: [uri, version, selections], }); }) );
|
この実装によって、クライアント側でlsp-sample.reverseが実行された際にサーバにlsp-sample.executeReverseコマンドと、uri、選択範囲を送信する処理が実現できました。
実装 (server.ts)
lsp-sample.executeReverseコマンドを受け取った際に引数情報を基に選択範囲を反転する処理を実装します。
まず以下のimportを追加します。
server.tsimport { /* 略 */ + Range } from "vscode-languageserver/node";
|
コマンド実行時に呼び出されるメソッドであるonExecuteCommand()
を追加します。
ここで送信されたドキュメントと取得したドキュメントのバージョンが一致するかどうか確認することで、非同期処理中の変更による処理のズレを防いでいます。
server.ts connection.onExecuteCommand((params) => { if ( params.command !== "lsp-sample.executeReverse" || params.arguments == null ) { return; } const uri = params.arguments[0].external; const textDocument = documents.get(uri); if (textDocument == null) { return; }
const version = params.arguments[1]; if (textDocument.version !== version) { return; }
const selections = params.arguments[1]; const changes: TextEdit[] = [];
for (const selection of selections) { const text = textDocument.getText(selection); if (text.length === 0) { continue; } const reversed = text.split("").reverse().join(""); changes.push(TextEdit.replace(selection, reversed)); }
if (changes.length === 0) { const text = textDocument.getText(); const reversed = text.split("").reverse().join(""); changes.push( TextEdit.replace( Range.create( Position.create(0, 0), textDocument.positionAt(text.length) ), reversed ) ); }
connection.workspace.applyEdit({ documentChanges: [ TextDocumentEdit.create( { uri: textDocument.uri, version: textDocument.version }, changes ), ], }); });
|
実装2: middlewareを用いた実装
処理の流れ
実装2ではmiddlewareという機能を用いて、クライアント側でコマンド実行情報に情報を追加することで実装します。
- ユーザがコマンドパレットでlsp-sample.reverseコマンドを実行する
- クライアント側で以下の情報を追加する
- アクティブなエディタが編集中のドキュメントのURI(識別子)
- アクティブなエディタが編集中のドキュメントのバージョン
- アクティブなエディタ上での選択範囲
- lsp-sample.reverseコマンドを検知したサーバは受け取った情報を基に選択範囲を反転する処理を実行する
実装 (extension.ts)
まず以下のimportを追加します。
extension.tsimport { workspace, ExtensionContext, + window, } from "vscode";
|
拡張機能の起動時に実行される関数activate()
のclientOptions
を以下のように変更します。
extension.ts const 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"), }, + middleware: { + executeCommand: async (command, args, next) => { + const uri = window.activeTextEditor.document.uri; + const version = window.activeTextEditor.document.version; + const selections = window.activeTextEditor.selections; + + return next(command, [...args, uri, version, selections]); + }, + }, };
|
実装 (server.ts)
lsp-sample.executeReverseコマンドを受け取った際に引数情報を基に選択範囲を反転する処理を実装します。
まず以下のimportを追加します。
server.tsimport { /* 略 */ + Range } from "vscode-languageserver/node";
|
connection.onInitializeのresultを以下のように変更し、サーバがlsp-sample.reverseの実行をサポートすることをクライアントに伝えます。
server.ts const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, // Tell the client that this server supports code completion. completionProvider: { resolveProvider: true, }, // コードアクション codeActionProvider: true, // フォーマット documentFormattingProvider: true, + // コマンド + executeCommandProvider: { + commands: ["lsp-sample.reverse"], + }, }, };
|
コマンド実行時に呼び出されるメソッドであるonExecuteCommand()
を追加します。
server.ts connection.onExecuteCommand((params) => { if ( params.command !== "lsp-sample.reverse" || params.arguments == null ) { return; } const uri = params.arguments[0].external; const textDocument = documents.get(uri); if (textDocument == null) { return; }
const version = params.arguments[1]; if (textDocument.version !== version) { return; }
const selections = params.arguments[1]; const changes: TextEdit[] = [];
for (const selection of selections) { const text = textDocument.getText(selection); if (text.length === 0) { continue; } const reversed = text.split("").reverse().join(""); changes.push(TextEdit.replace(selection, reversed)); }
if (changes.length === 0) { const text = textDocument.getText(); const reversed = text.split("").reverse().join(""); changes.push( TextEdit.replace( Range.create( Position.create(0, 0), textDocument.positionAt(text.length) ), reversed ) ); }
connection.workspace.applyEdit({ documentChanges: [ TextDocumentEdit.create( { uri: textDocument.uri, version: textDocument.version }, changes ), ], }); });
|
動作確認
任意の範囲を選択肢、コマンドパレットでreverse textを実行すると、選択範囲が反転されます。また、範囲を選択していない場合はドキュメント全体が反転されます。
まとめ
LSPを用いた拡張機能のサンプルコードに機能を追加する方法を解説しました。
LSPを用いたVSCodeの拡張機能の開発に関する日本語記事はまだまだ少ないので、この記事が少しでも開発の助けになれば幸いです。
参考文献