はじめに
プログラマの仕事道具といえばソースコードエディタ(以下,エディタ)です.エディタ開発はプログラマの能力を最大化するための有効な手段と言えます.本記事はエディタ開発の第一歩して拡張機能,またはプラグインの開発を行います.特にEclipse登場以降のプログラミングエディタの多くは拡張機能の集合体として設計されているため,拡張機能開発の経験はエディタ本体の開発にも応用可能です.
本記事ではLanguage Server Protocol (以下,LSP)を用いたエディタの拡張機能開発を行います.LSPとは,コード補完や,変数参照,スタイル修正といった機能実装をあらゆるエディタ/IDE へ提供するプロトコルです.本記事ではVisual Studio Codeを用いて拡張機能開発を行いますが,作成した機能はVimやEmacsでも使えます.
一応公式チュートリアルもありますが,拡張機能の開発経験を前提とした高度な内容なため,ここではその内容を簡単にした例でエディタ開発を学んでいきます.
注意: 本記事はLanguage Server Protocolの開発方法を紹介する記事であり,利用方法については他に詳しいサイトを記事終盤で紹介しています
対象読者
- エディタ開発に興味がある人
- エディタの構造を知りたい人
- 色んなエディタをいじってみたい人
もしこの記事の内容が難しすぎる場合,以下のリンクから基礎的な学習を行えます.
この記事ではTypeScriptを利用していますが,あまり高度な機能は利用しないため,JavaScript,もしくは他の言語にふれたことがあれば大丈夫です.
今回行うこと
行わないこと
- 言語依存の機能(補完, 参照)開発
- リモート環境・マルチルートワークスペースを考慮した機能開発
開発環境
- Visual Studio Code
- Node JS x64, version >= 10.x, <= 12.x
Hello World
コンピュータープログラミングにおける古来の伝統に従い,最初に作るアプリケーションは「Hello World」を表示するプログラムにしましょう.
簡素なテンプレートを用意しましたのでそれをクローンしてVS Codeで開きましょう.
git clone https://github.com/Ikuyadeu/vscode-language-server-template.git
code vscode-language-server-template
ビルド
1: ターミナルを起動
[表示] -> [統合ターミナル]
または,Control
+ ` (Macなら ^
+ `)
2: 以下のコマンドを実行し,必要なパッケージのインストールを行います.
npm install
3: F5
キーを入力することで拡張機能をインストールしたVS Codeを立ち上げます.
4: 開いたエディタ上で.txt
ファイル,もしくは.md
ファイルを開いてみましょう.
画像では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(説明書)
ソースコード解説
サーバー側のソースコード
"use strict";
import {
CodeActionKind,
createConnection,
Diagnostic,
DiagnosticSeverity,
Range,
TextDocuments,
TextDocumentSyncKind,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
namespace CommandIDs {
export const fix = "sample.fix";
}
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
const connection = createConnection();
connection.console.info(`Sample server running in node ${process.version}`);
let documents!: TextDocuments<TextDocument>;
// 初期化
connection.onInitialize(() => {
documents = new TextDocuments(TextDocument);
setupDocumentsListeners();
// サーバー全体の機能を記述する
return {
capabilities: {
// ファイルを保存したときや変更,閉じたときに実行
textDocumentSync: {
openClose: true,
change: TextDocumentSyncKind.Incremental,
willSaveWaitUntil: false,
save: {
includeText: false,
},
},
// 自動修正を実装よてい
codeActionProvider: {
codeActionKinds: [CodeActionKind.QuickFix],
},
executeCommandProvider: {
commands: [CommandIDs.fix],
},
},
};
});
/**
* ソースコードの一行目に対して警告を表示する
*/
function validate(doc: TextDocument) {
// 警告などファイルの状態を管理するリスト
const diagnostics: Diagnostic[] = [];
// 警告を出す範囲(今回は0行目の0文字目から0行目の最終文字目まで)
const range: Range = {start: {line: 0, character: 0},
end: {line: 0, character: Number.MAX_VALUE}};
// 警告を追加,引数は順番に範囲,メッセージ,警告強度,警告id,警告のソース
diagnostics.push(Diagnostic.create(range, "Hello world", DiagnosticSeverity.Warning, "", "sample"));
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}
function setupDocumentsListeners() {
documents.listen(connection);
// ファイルを開いたとき,変更したときにファイルを検証し,警告を表示する
documents.onDidOpen((event) => {
validate(event.document);
});
documents.onDidChangeContent((change) => {
validate(change.document);
});
// ファイルを閉じたとき,警告表示を消す
documents.onDidClose((close) => {
connection.sendDiagnostics({ uri: close.document.uri, diagnostics: []});
});
}
// Listen on the connection
connection.listen();
クライアント側のソースコード
"use strict";
import * as path from "path";
import { ExtensionContext, window as Window } from "vscode";
import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions, TransportKind } from "vscode-languageclient";
// 拡張機能を立ち上げたときに呼び出す関数
export function activate(context: ExtensionContext): void {
const serverModule = context.asAbsolutePath(path.join("server", "out", "server.js"));
const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"], cwd: process.cwd() };
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc, options: { cwd: process.cwd() } },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions,
},
};
// 対象とする言語.今回は.txtファイルと.mdファイル
const clientOptions: LanguageClientOptions = {
documentSelector: [
{
scheme: "file",
language: "plaintext",
},
{
scheme: "file",
language: "markdown",
}],
diagnosticCollectionName: "sample",
revealOutputChannelOn: RevealOutputChannelOn.Never,
};
let client: LanguageClient;
try {
client = new LanguageClient("Sample LSP Server", serverOptions, clientOptions);
} catch (err) {
Window.showErrorMessage("The extension couldn't be started. See the output channel for details.");
return;
}
client.registerProposedFeatures();
context.subscriptions.push(
client.start(),
);
}
警告機能の実装
次に,大文字に対して警告文を出す実装を行います.
サーバーのソースコードserver/src/server.ts
の関数validate
を以下の通りに書き換えてみましょう.
/**
* 大文字に対して警告を表示する
*/
function validate(doc: TextDocument) {
// 2つ以上並んでいるアルファベット大文字を検出
const text = doc.getText();
// 検出するための正規表現 (正規表現テスト: https://regex101.com/r/wXZbr9/1)
const pattern = /\b[A-Z]{2,}\b/g;
let m: RegExpExecArray | null;
// 警告などの状態を管理するリスト
const diagnostics: Diagnostic[] = [];
// 正規表現に引っかかった文字列すべてを対象にする
while ((m = pattern.exec(text)) !== null) {
// 対象の位置から正規表現に引っかかった文字列までを対象にする
const range: Range = {start: doc.positionAt(m.index),
end: doc.positionAt(m.index + m[0].length),
};
// 警告内容を作成,上から範囲,メッセージ,重要度,ID,警告原因
const diagnostic: Diagnostic = Diagnostic.create(
range,
`${m[0]} is all uppercase.`,
DiagnosticSeverity.Warning,
"",
"sample",
);
// 警告リストに警告内容を追加
diagnostics.push(diagnostic);
}
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}
もう一度F5
キーで実行し以下のファイルを作ってみましょう
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 shost. ANY OS. Open Source.
ファイル中の大文字ANY
やOS
に対して以下のように波線で警告が出るはずです.
自動修正機能の実装
警告だけに終わらず,自動的に修正する機能を実装していきます.今回は大文字を小文字にしていきます.
引き続き,server/src/server.ts
を編集します.import
以下と関数setupDocumentsListeners
を以下のコードに編集してみましょう.ここまでの過程で,つまづいた場合は出来上がったものをcompleted
ブランチとして用意してありますのでそちらをご利用ください.
import {
CodeAction,
CodeActionKind,
createConnection,
Diagnostic,
DiagnosticSeverity,
Range,
TextDocumentEdit,
TextDocuments,
TextDocumentSyncKind,
TextEdit,
} from "vscode-languageserver";
function setupDocumentsListeners() {
documents.listen(connection);
documents.onDidOpen((event) => {
validate(event.document);
});
documents.onDidChangeContent((change) => {
validate(change.document);
});
documents.onDidClose((close) => {
connection.sendDiagnostics({ uri: close.document.uri, diagnostics: []});
});
// Code Actionを追加する
connection.onCodeAction((params) => {
// sampleから生成した渓谷のみを対象とする
const diagnostics = params.context.diagnostics.filter((diag) => diag.source === "sample");
// 対象ファイルを取得する
const textDocument = documents.get(params.textDocument.uri);
if (textDocument === undefined || 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;
});
}
公開方法
以下はそれぞれ各リンクを参照ください.
VS Code 拡張機能の公開
まとめると以下のコマンドです.
vsce login
vsce publish
Language Serverの公開
npmパッケージとして公開します.
まとめると以下のコマンドです.
cd server
npm login
npm publish
他エディタでもLSPを利用
以下のコマンドでLanguage Serverのnpmパッケージをインストールすることで他のエディタでも開発したLSPを利用できます.
npm install -g {公開したLanguage Server}
エディタごとの利用方法リンク
発展
公式が提供しているLSPの実装例は以下のとおりです.
更に新しい機能を作りたい場合はこれらを参考に発展させていきましょう.
サンプル | 内容 |
---|---|
LSP Sample | 小文字のアルファベットを見つけると注意する.本記事の内容に加え,設定ファイルへのアクセスなども行う |
LSP Multi Root Server Sample | マルチルートワークスペースごとにLSPを立ち上げる,LSP Sampleの拡張版 |
LSP Log Streaming Sample | LSP実行時にログを出力する, LSP Sampleの拡張版 |
LSP UI Example | ファイルの1行目に文字の挿入を行うアクションを追加する |
より高度な開発を体験するには各言語サーバーのOSSに参加するのが一番だと思います.
参考文献
宣伝
- GitHubアプリ作成チュートリアルの翻訳記事を公開しています.本記事と合わせて読むことでオフライン・オンライン両方で開発を効率化できます.
- LSPを使ったVS Codeの拡張機能を公開しています.本記事で紹介したソースコードの発展版となります.よければ使ってみてください.