本コースではLanguage Server Protocol (以下,LSP)を用いたエディタの拡張機能開発を行います. LSPとは,コード補完や,変数参照,スタイル修正といった機能実装をあらゆるエディタ/IDE へ提供するプロトコルです.
- Visual Studio Code
- Node JS x64, version >= 16.x, < 17.x
コンピュータープログラミングにおける古来の伝統に従い,最初に作るアプリケーションは「Hello World」を表示するプログラムにしましょう.
簡素なテンプレートを用意しましたのでそれをクローンしてVS Codeで開きましょう.
git clone https://github.com/vscodejp/vscode-language-server-template.git
code vscode-language-server-template
1: ターミナルを起動
[表示] -> [統合ターミナル]
または,Control
+ ` (Macなら ^
+ `)
2: 以下のコマンドを実行し,必要なパッケージのインストールを行います.
npm install
3: サイドバーのデバッグ(上から4番目のアイコン)からLaunch Client
を選択し,Client+Server
を選び実行ボタンを押す (または単にF5
キーを入力する)ことで拡張機能をインストールしたVS Codeを立ち上げます.
4: 開いたエディタ上で.txt
ファイル,もしくは.md
ファイルを開いてみましょう.ない場合は新たにtest.txt
などの文章を作成しましょう.
画像ではtest.txt
の1行目に波線を表示させ,その上にマウスを置くとHello World
と表示させています.
以下がVS CodeでLSPを実装するときの一般的なファイルの構成です.
本記事ではサーバー側のメインであるserver/src/server.ts
を編集していきます.
.
├── client // Language Serverのクライアントサイド
│ ├── src
│ │ └── extension.ts // クライアント側拡張機能のソースコード
│ └── package.json // クライアント側として利用するパッケージ情報
│
├── server // Language Serverのサーバーサイド
│ │── src
│ │ └── server.ts // Language Serverのメインソースコード
│ │── package.json // クライアント側として利用するパッケージ情報
│ └── README.md // サーバーとしてのREADME.md (説明書)
│
├── package.json // VS Codeプラグインとしてのパッケージ情報
└── README.md // VS CodeプラグインとしてのREADME.md(説明書)
今回は起動条件 (activationEvents) に onLanguage:plaintext
とonLanguage:markdown
つまり,プレーンテキストやマークダウンファイルを(拡張子が.txtまたは.md)開いた時を追加しています.
...
"activationEvents": [
"onLanguage:plaintext",
"onLanguage:markdown"
],
...
実行環境はvscode
の1.61.0以上を前提としています.
バージョンによってはAPIの利用方法が変わるので注意です.
...
"engines": {
"vscode": "^1.61.0"
},
...
利用する依存関係として,Language Serverと連携するvscode-languageclient
と, UIなどエディタ側の機能を提供するvscode
を使います.
...
"devDependencies": {
"@types/vscode": "1.61.0"
},
"dependencies": {
"vscode-languageclient": "8.0.0-next.4"
}
...
こちらは長めなので展開式にしています.
クライアント側のソースコードファイルは通常のVS Code拡張機能として実装しているため,VS Code APIを利用します.
現在バージョンのクライアントパッケージはvscode-languageclient/node
から呼び出せます.vscode-languageclient
と間違えないよう注意してください.
クライアント側のソースコード`client/src/extension.ts`
'use strict';
import { ExtensionContext, window as Window, Uri } from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
RevealOutputChannelOn,
ServerOptions,
TransportKind } from 'vscode-languageclient/node';
// 拡張機能が有効になったときに呼ばれる
export function activate(context: ExtensionContext): void {
// サーバーのパスを取得
const serverModule = Uri.joinPath(context.extensionUri, 'server', 'out', 'server.js').fsPath;
// デバッグ時の設定
const debugOptions = { execArgv: ['--nolazy', '--inspect=6011'], cwd: process.cwd() };
// サーバーの設定
const serverOptions: ServerOptions = {
run: {
module: serverModule,
transport: TransportKind.ipc,
options: { cwd: process.cwd() }
},
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions,
},
};
// LSPとの通信に使うリクエストを定義
const clientOptions: LanguageClientOptions = {
// 対象とするファイルの種類や拡張子
documentSelector: [
{ scheme: 'file' },
{ scheme: 'untitled' }
],
// 警告パネルでの表示名
diagnosticCollectionName: 'sample',
revealOutputChannelOn: RevealOutputChannelOn.Never,
initializationOptions: {},
progressOnInitialization: true,
};
let client: LanguageClient;
try {
// LSPを起動
client = new LanguageClient('Sample LSP Server', serverOptions, clientOptions);
} catch (err) {
void Window.showErrorMessage('拡張機能の起動に失敗しました。詳細はアウトプットパネルを参照ください');
return;
}
// 拡張機能のコマンドを登録
context.subscriptions.push(
client.start(),
);
}
サーバー側のソースコードはserver/src/server.ts
とserver/src/package.json
です.
これらはクライアント側の依存関係とは独立しています.
サーバー側のソースコード`server/src/server.ts`
'use strict';
import {
createConnection,
Diagnostic,
DiagnosticSeverity,
ProposedFeatures,
Range,
TextDocuments,
TextDocumentSyncKind,
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
// サーバー接続オブジェクトを作成する。この接続にはNodeのIPC(プロセス間通信)を利用する
// LSPの全機能を提供する
const connection = createConnection(ProposedFeatures.all);
connection.console.info(`Sample server running in node ${process.version}`);
// 初期化ハンドルでインスタンス化する
let documents!: TextDocuments<TextDocument>;
// 接続の初期化
connection.onInitialize((_params, _cancel, progress) => {
// サーバーの起動を進捗表示する
progress.begin('Initializing Sample Server');
// テキストドキュメントを監視する
documents = new TextDocuments(TextDocument);
setupDocumentsListeners();
// 起動進捗表示の終了
progress.done();
return {
// サーバー仕様
capabilities: {
// ドキュメントの同期
textDocumentSync: {
openClose: true,
change: TextDocumentSyncKind.Incremental,
willSaveWaitUntil: false,
save: {
includeText: false,
}
}
},
};
});
/**
* テキストドキュメントを検証する
* @param doc 検証対象ドキュメント
*/
function validate(doc: TextDocument) {
// 警告などの状態を管理するリスト
const diagnostics: Diagnostic[] = [];
// 0行目(エディタ上の行番号は1から)の端から端までに警告
const range: Range = {start: {line: 0, character: 0},
end: {line: 0, character: Number.MAX_VALUE}};
// 警告を追加する
const diagnostic: Diagnostic = {
// 警告範囲
range: range,
// 警告メッセージ
message: 'Hello world',
// 警告の重要度、Error, Warning, Information, Hintのいずれかを選ぶ
severity: DiagnosticSeverity.Warning,
// 警告コード、警告コードを識別するために使用する
code: '',
// 警告を発行したソース、例: eslint, typescript
source: 'sample',
};
diagnostics.push(diagnostic);
//接続に警告を通知する
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}
/**
* ドキュメントの動作を監視する
*/
function setupDocumentsListeners() {
// ドキュメントを作成、変更、閉じる作業を監視するマネージャー
documents.listen(connection);
// 開いた時
documents.onDidOpen((event) => {
validate(event.document);
});
// 変更した時
documents.onDidChangeContent((change) => {
validate(change.document);
});
// 保存した時
documents.onDidSave((change) => {
validate(change.document);
});
// 閉じた時
documents.onDidClose((close) => {
// ドキュメントのURI(ファイルパス)を取得する
const uri = close.document.uri;
// 警告を削除する
connection.sendDiagnostics({ uri: uri, diagnostics: []});
});
}
// Listen on the connection
connection.listen();