VSCode拡張機能を作ってみる3 - 文字の装飾

# はじめに

[公式サンプル](https://github.com/microsoft/vscode-extension-samples)にVSCode APIを使ったサンプルがあるので、いくつか使いそうなものをピックアップしてみました。

今回は、`Decorations` API を使ってハイライトや文字色などの装飾をする機能を付けます。

https://github.com/microsoft/vscode-extension-samples/tree/main/decorator-sample

前回
[VSCode拡張機能を作ってみる1 - インストールとHelloWorld!](https://qiita.com/qvtec/items/53ec5a7dd473142c69c1)
[VSCode拡張機能を作ってみる2 - 入力補完](https://qiita.com/qvtec/items/31d19dd8b86fcc19465a)

# 事前準備

まず、空の`decorator.ts`ファイルを作成し、`extension.ts` で読み込んでおきます。
ファイルを分けたかったので、公式から結構変更がありますが、やっていることは同じです。

エディタの変更が合った場合とテキストの変更があった場合に、Decolator処理を実行します。

``` ts:extension.ts
import * as vscode from 'vscode';
import Decorator from './decorator';

export function activate(context: vscode.ExtensionContext) {
    let decorator = new Decorator();

    vscode.window.onDidChangeActiveTextEditor(editor => {
        decorator.changeActiveTextEditor(editor);
    }, null, context.subscriptions);

    vscode.workspace.onDidChangeTextDocument(event => {
        decorator.changeTextDocument(event);
    }, null, context.subscriptions);
}

export function deactivate() {}
```

エディタの変更が合った場合には`changeActiveTextEditor`、テキストの変更があった場合には`changeTextDocument`を実行しています。
どちらもtimeout設定を経由して`updateDecorations`のメイン処理を実行しています。
constructorで`setDecorators`にあるスタイル設定を事前におこなっています。

``` ts:decorator.ts
import * as vscode from 'vscode';

export default class Decolator
{
    private activeEditor = vscode.window.activeTextEditor;
    private timeout: NodeJS.Timer | undefined = undefined;
    private decorationTypes: vscode.TextEditorDecorationType[] = [];

    constructor() {
        this.setDecorators();

        if (this.activeEditor) {
            this.triggerUpdateDecorations();
        }
    }

    public changeActiveTextEditor(editor: vscode.TextEditor | undefined): void {
        this.activeEditor = editor;
        if (editor) {
            this.triggerUpdateDecorations();
        }
    }

    public changeTextDocument(event: vscode.TextDocumentChangeEvent): void {
        if (this.activeEditor && event.document === this.activeEditor.document) {
            this.triggerUpdateDecorations();
        }
    }

    private triggerUpdateDecorations(): void {
        if (this.timeout) {
            clearTimeout(this.timeout);
            this.timeout = undefined;
        }
        this.timeout = setTimeout(() => {
            this.updateDecorations();
        }, 500);
    }

    private updateDecorations(): void {
        if (!this.activeEditor) {
            return;
        }

        // ここでDecorationsを実行します
    }

    private setDecorators(): void {
        // ここでスタイルの設定をおこないます
    }
}
```

# 数値の装飾サンプル

まずは公式サンプルにある数値の装飾をしてみます。

スタイルの設定が2パターンあります。
`smallNumberDecorationType`はハードコーディングされていますが、
`largeNumberDecorationType`は`package.json`の`contributes.colors`に定義された情報を参照しています。
そうしておけば、ユーザー設定で色を再定義できます。

``` ts:decorator.ts
    private setDecorators(): void {
        let smallNumberDecorationType = vscode.window.createTextEditorDecorationType({
            borderWidth: '1px',
            borderStyle: 'solid',
            overviewRulerColor: 'blue',
            overviewRulerLane: vscode.OverviewRulerLane.Right,
            light: {
                // this color will be used in light color themes
                borderColor: 'darkblue'
            },
            dark: {
                // this color will be used in dark color themes
                borderColor: 'lightblue'
            }
        });
        this.decorationTypes.push(smallNumberDecorationType);

        let largeNumberDecorationType = vscode.window.createTextEditorDecorationType({
            cursor: 'crosshair',
            // use a themable color. See package.json for the declaration and default values.
            backgroundColor: { id: 'myextension.largeNumberBackground' }
        });
        this.decorationTypes.push(largeNumberDecorationType);
    }
```

regExで指定した数字を正規表現で取得し、3以下なら`smallNumbers`、それ以外は`largeNumbers`として対象ヵ所のrangeとhoverMessageの設定をしています。

最後に`setDecorations`で対象ヵ所にスタイルを反映することができます。

``` ts:decorator.ts
    private updateDecorations(): void {
        if (!this.activeEditor) {
            return;
        }
        const regEx = /\d+/g;
        const text = this.activeEditor.document.getText();
        const smallNumbers: vscode.DecorationOptions[] = [];
        const largeNumbers: vscode.DecorationOptions[] = [];
        let match;
        while ((match = regEx.exec(text))) {
            const startPos = this.activeEditor.document.positionAt(match.index);
            const endPos = this.activeEditor.document.positionAt(match.index + match[0].length);
            const decoration = { range: new vscode.Range(startPos, endPos), hoverMessage: 'Number **' + match[0] + '**' };
            if (match[0].length < 3) {
                smallNumbers.push(decoration);
            } else {
                largeNumbers.push(decoration);
            }
        }

        this.activeEditor.setDecorations(this.decorationTypes[0], smallNumbers);
        this.activeEditor.setDecorations(this.decorationTypes[1], largeNumbers);
    }
```

# まとめ

highlightの拡張機能はたくさんあるので、あえて作ることは少ないとは思うが、オレオレ言語だったり、そのほかの作りたい拡張機能と組み合わせると便利なものになりそう。