Skip to content

Latest commit

 

History

History
302 lines (248 loc) · 10.5 KB

File metadata and controls

302 lines (248 loc) · 10.5 KB

Hello Language Server Protocol

はじめに

本コースではLanguage Server Protocol (以下,LSP)を用いたエディタの拡張機能開発を行います. LSPとは,コード補完や,変数参照,スタイル修正といった機能実装をあらゆるエディタ/IDE へ提供するプロトコルです.

今回行うこと

開発環境

Hello World

コンピュータープログラミングにおける古来の伝統に従い,最初に作るアプリケーションは「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と表示させています.

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(説明書)

ソースコード解説

拡張機能のメタファイルpackage.json

今回は起動条件 (activationEvents) に onLanguage:plaintextonLanguage: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.tsserver/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();