Visual Studio Code向けの簡易スペルチェッカーを実装してみる

2016-12-19

この記事はVisual Studio Code Advent Calendar 2016の19日目の記事です。


この記事では、Visual Studio Codeにおいて…

といっても、VSCodeのExtension APIを叩くだけですし、 世の中には実用的なスペルチェッカーのソースコードが公開されているので、 それを例にソースコードを読み解いていきたいと思います。

ちなみに去年のAdvent Calendarでは、VSCodeの画面をリアルタイムで共有できるサービスを作って公開したりしましたが、今年は時間が全然なかったので何もできませんでした。すみません。

あとこの記事はだいぶ急いで書いています。 かなり殴り書きのようになってしまいましたがご容赦ください。 またご意見等ありましたら、コメントやTwitterなどでいただければと思います。

完成版はこちらからどうぞ。

https://github.com/bonprosoft/SimpleTypoChecker

スペルチェッカの動作例

スペルチェッカの動作例

診断機能を実装する

Visual Studio Codeで診断機能を実装するには、APIを通してvscode.DiagnosticCollectionを確保した後(4行目)、このコレクションに診断結果を格納する必要があります。

拡張機能それぞれで解析処理を行った結果、ユーザーに診断結果を通知する必要がある場合には、 ドキュメント中の表示する位置を求めた後(18-20行目)、診断結果を使って作成します。(21行目)

ここでDiagnosticSeverityの値をInformationやErrorなどに変更すると、それに合わせてメッセージのアイコンが変わります。

これらによって作成された診断結果のリストを、ドキュメントのURIと対応付けてコレクションへと格納します。(28行目)

checker.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private _diagnostics: vscode.DiagnosticCollection;

activate() {
    this._diagnostics = vscode.languages.createDiagnosticCollection('STC');
}

private createDiagnostics(document: vscode.TextDocument) {
    let diagnostics: vscode.Diagnostic[] = [];
    let docText = document.getText();

    // [Optional] コメント行を除きます
    docText = docText.replace(/<!--(.*?)-->/g, "");

    // 何らかの診断・解析処理を行います
    // // posには診断結果を表示したい位置が格納されているとします
    // // 以下では例として最初にHelloが検出された位置を格納します
    // let index = docText.indexOf("Hello");
    // let pos: [number, number] = [index, index + 5];

    let startPos = document.positionAt(pos[0]);
    let endPos = document.positionAt(pos[1]);
    let range = new vscode.Range(startPos, endPos);
    let diag = new vscode.Diagnostic(range, "This is a message", vscode.DiagnosticSeverity.Warning);
    diagnostics.push(diag);
    // -> もちろんdiagnosticsにさらに項目を追加することも可能です

    // DiagnosticCollectionにドキュメントのURIと診断結果を格納
    this._diagnostics.set(document.uri, diagnostics);
}

あとは、新しくエディタが開かれたとき(vscode.window.onDidChangeActiveTextEditor)やドキュメントが保存されたとき(vscode.workspace.onDidSaveTextDocument)に発火するイベントをトリガーにして、createDiagnostics関数を呼び出してあげましょう。

実際にこの時点でコードを実行すると次のように診断結果が表示されることがわかります。

1つ目のHelloのみを検出する診断機能

1つ目のHelloのみを検出する診断機能

コードアクションを実装する

コードアクションを実装するのは少々複雑です。コードアクションは以下の手順で実現されます。

  1. CodeActionProviderインターフェースを実装したクラスを用意する
  2. 1.で用意したクラスをvscode.languages.registerCodeActionsProvider APIを用いて登録する
    • この時、どの言語に対してコードアクションを提供するかを指定する
  3. コードアクション用のコマンドを登録する
  4. CodeActionProviderのメンバーであるprovideCodeActionメソッドを実装する
    • このメソッドの返り値で、コードアクションとして提供するアクションコマンドのリストを提供する
    • アクションコマンドには、採用された際に実行されるコマンドのIdを指定する
    • ここまでで、診断機能が表示された箇所にLight bulb(電球マーク)が表示されるようになる
  5. アクションコマンドが実行された際に呼ばれるコマンドを実装する

以下、順を追って説明していきます。ソースコードの完成版は以下のリンクにありますので、併せて確認すると良いでしょう。

SimpleTypoChecker/checker.ts

CodeActionProviderインターフェースを実装したクラスを用意する

そのままです

checker.ts
1
2
export default class DocumentChecker implements vscode.CodeActionProvider, vscode.Disposable {

1.で用意したクラスをvscode.languages.registerCodeActionsProvider APIを用いて登録する

第1引数にlanguage Id, 第2引数にCodeActionProviderインターフェースを実装したクラスのインスタンスを指定します。

checker.ts
1
2
3
4
5
6
7
activate() {
    // 前略
    vscode.languages.registerCodeActionsProvider("markdown", this);
    vscode.languages.registerCodeActionsProvider("plaintext", this);
    // 後略
}

コードアクション用のコマンドを登録する

コード修正用のコマンドを登録します。ここで登録したコマンドが、コードアクションによって表示されるLight bulbを実際に採用した際に行われる処理の内容となります。

この時、コールバックとなる関数はfixWithSuggestionという名前にしたため、5.でこの関数の中身を実装する必要があります。

checker.ts
1
2
3
4
5
6
activate() {
    // 前略
    this._fixOnSuggestionCommand = vscode.commands.registerCommand("simpletypochecker.fixOnSuggestion", this.fixWithSuggestion.bind(this));
    // 後略
}

CodeActionProviderのメンバーであるprovideCodeActionメソッドを実装する

いい感じに実装します。ここでポイントとなるのが8-12行目です。

10行目では、採用された際に実行されるコマンド(a)のId(先ほどの項目で使ったものと同じである必要があります)を指定し、11行目ではその関数へと渡す引数を指定しています。

checker.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.Command[] {

    let diagnostic: vscode.Diagnostic = context.diagnostics[0];
    let error: string = document.getText(range);
    let suggestion: string = "See you";
    let commands: vscode.Command[] = [];

    commands.push({
        title: 'Replace with \'' + suggestion + '\'',
        command: "simpletypochecker.fixOnSuggestion",
        arguments: [document, diagnostic, suggestion]
    });

    return commands;
}

アクションコマンドが実行された際に呼ばれるコマンドを実装する

いよいよ最後の段階まで来ました。

この中ではコードアクションの実行部分、つまり実際に指定された箇所の変更を行うコードを記述していきます。

ここでポイントとなるのが、9-10行目です。

9行目でドキュメントのURI・範囲・編集内容を記録した後、10行目で実際に記録された内容の適用を行っています。

checker.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private fixWithSuggestion(document: vscode.TextDocument, diagnostic: vscode.Diagnostic, suggestion: string): any {
    let docError: string = document.getText(diagnostic.range);

    let diagnostics: vscode.Diagnostic[] = this._diagnosticMap[document.uri.toString()];
    let index: number = diagnostics.indexOf(diagnostic);

    let edit = new vscode.WorkspaceEdit();
    edit.replace(document.uri, diagnostic.range, suggestion);
    return vscode.workspace.applyEdit(edit);
}

ここまで出来たら、実際に実行してみましょう。

“Hello”が検出されて、”See you”に置換するコードアクションが表示されることでしょう。また実際にLight bulbを採用すると、コードアクションが適用されることがわかります。

おちばびろい

コードアクションを適用したのに診断結果の波線が消えない…!

こんな感じに、拡張機能側でもmapを持っておいて、コードアクションを適用したら修正/再適用してあげると良い感じになります。

保存中に診断が実行されて重い…!

こちらのソースコードをご覧ください。

この実装方法は、MS公式のvscode-spell-checkを参考にしたものですが、診断処理を行うタスクをわざと一定期間遅延させて、その間に起った診断処理の再計算を求める処理をすべて1つにまとめています。

これによって短期間に連続して何度も診断処理を行うことや、保存中にフリーズすることを防いでいます。

そもそも処理が遅い

頑張って良い感じのデータ構造・アルゴリズムを使って高速化してください。

テキストが動的に変わって、スペルチェック用のファイルがほぼ変わらないので、AC Automatonでも作ると良いんじゃないんですかね、というのがらぼでの見解でした。

まとめ

いかがでしょうか。わりと簡単に診断機能とコードアクションを使うことができることがお分かりいただけたかと思います。 実はこの拡張機能自体は今年の9月頃に作成しましたが、主に執筆時の表記ゆれを検出するのに(個人的には)大活躍しています。

非常に便利な診断機能/コードアクション向けのAPI、ぜひ何かの拡張機能で活用していきたいものですね。

このエントリーをはてなブックマークに追加
« 「NuGetでプラットフォーム毎にアセンブリを展開する方法」と「良い感じにコードを共有してプラットフォーム別のアセンブリを作る方法」 たるしょ~ Advent Calendar 19日目 »