VSCode 拡張機能作成に入門してみたよ（setDecorations 編）

# はじめに

* ちょっとした VSCode の拡張機能のアイデアを思いついたので、VSCode 拡張機能に入門(?)してみました。
* 今回使いたい機能は、主に文字の装飾 `setDecorations()` がメインだったので、調べてみたことをアウトプットしてみます。
* ついでに、今回作った拡張機能も紹介させてください。

# VSCode 開発環境の構築

基本的にいろいろなサイトや記事があると思うので、詳細は折りたたんでおきます。自分用のメモです。

<details><summary>環境構築とプロジェクトフォルダ作成</summary>

当然、node.js のインストールが必要です。またバージョンが古い場合にはアップグレードが必要です。方法はいろいろあると思うので、各自の環境や好みに合わせればよいと思います。
私の場合は、以前 `n` コマンドで node.js をインストールしていたみたいなので、必要であればそれでアップグレードをすることになります。

また、 VSCode も最新にしておいた方が良いだろうと思います。

次に、必要なツール・ライブラリをインストールします。

```
sudo npm install -g yo generator-code @vscode/vsce
```

あるいは既にインストール済みでも、久しぶりならバージョンアップする必要があると思うので、やはり上記のコマンドを実行します。

最後に、適当なディレクトリにプロジェクトを作成します（実行するとプロジェクト用のディレクトリが新しくできるので、それを考慮して決定）。

```
yo code
```

いろいろ聞かれますがとりあえず `New Extension (TypeScript)` を選択して、プロジェクト名（拡張機能名）を適当に決めて、後はお好みで進めて、しばらく待つと、カレントディレクトリにプロジェクト名で新しいディレクトリができます。

</details>

# setDecorations() について

`setDecorations()` は、`vscode.TextEditor` のメソッドで、テキストエディタ上の文字を装飾する機能です。
例えば予約語の前景色を変えたり、インデントや末尾の空白の背景色を変えたりする拡張機能があると思いますが、そのようなことができます。

しかし、調べてみるともう少しいろいろなことができることが分かったので、自分のためにもメモを残しておきます。

## 参考情報

オフィシャルな API ドキュメントについては以下のページを参考にしてください。
API 以外にも、Extension Manifest (`package.json` の記載方法)や、Contribution Points、その他の項目もあります。

https://code.visualstudio.com/api/references/vscode-api

また、VSCode の拡張機能には、いろいろな公式サンプルコードがあり、`setDecorations()` に関しては以下のサンプルコードが非常に参考になると思います。

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

## 前景色(color)と背景色(backgroundColor)など

サンプルコードを参考に以下のような拡張機能を作ってみました。

```typescript:extension.ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    console.log("activate");
    const regex = /\bTODO\b/g;
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
    });

    function updateDecorations(editor: vscode.TextEditor) {
        console.log("updateDecorations");
        const text = editor.document.getText();
        const options: vscode.DecorationOptions[] = Array.from(
            text.matchAll(regex),
            match => ({
                range: new vscode.Range(
                    editor.document.positionAt(match.index!),
                    editor.document.positionAt(match.index! + match[0].length))
            }));
        editor.setDecorations(decorationType, options);
    }

    const updateDecorationsIfPossible = (): void => {
        const editor = vscode.window.activeTextEditor;
        if (editor) {
            updateDecorations(editor);
        }
    };

    let timeout: NodeJS.Timer | undefined = undefined;
    function triggerUpdateDecorations(throttle = false) {
        if (timeout) {
            clearTimeout(timeout);
            timeout = undefined;
        }
        if (throttle) {
            timeout = setTimeout(updateDecorationsIfPossible, 500);
        } else {
            updateDecorationsIfPossible();
        }
    }

    triggerUpdateDecorations();

    vscode.window.onDidChangeActiveTextEditor(_editor => {
        console.log("onDidChangeActiveTextEditor");
        triggerUpdateDecorations();
    }, null, context.subscriptions);

    vscode.workspace.onDidChangeTextDocument(event => {
        console.log("onDidChangeTextDocument");
        if (event.document === vscode.window.activeTextEditor?.document) {
            triggerUpdateDecorations(true);
        }
    }, null, context.subscriptions);
}
```

イベントや細かい動作については割愛しますが、簡単に概要だけ説明すると……。

* `TextEditorDecorationType` は事前に（`activate()` が呼び出された時に）作っておきます。
* ドキュメントが最初に表示された時や、書き換わった場合に、文字の装飾を変更します。
* ただし、ドキュメントが書き換わった場合には、連続してイベントが発生する可能性（文字を連続して入力するなど）があるため、ディレイタイム（ここでは 500ms）を設けておいて、それ以内に再度イベントが発生した場合にはディレイ中の処理をキャンセルして、再度処理を `setTimeout()` で登録します。ディレイタイムを小さくした方が色の切り替わりは早くなりますが、装飾更新の処理が何度も走るので処理コストが増えます。500ms はちょっとのんびり目かな？ いくつかの拡張機能だと 100ms ぐらいがデフォルトになっているようでした。10ms だと小さすぎる感じ。

ソースのポイントは以下です。

```typescript
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
    });
```

`color` が前景色、`backgroundColor` が背景色を指定する箇所です。

また `package.json` には以下の修正をします。

```js
    "activationEvents": [
        "onLanguage:plaintext"
    ],
    "main": "./dist/extension.js",
    "contributes": {
    },
```

今回はテスト用にプレーンテキストの場合に有効にしています。

実際にこの拡張機能を動かすと以下のようになります。
プレーンテキストのファイルに `テスト TODO` という文字を入れたところです。
`TODO` の部分の前景色・背景色が変わっているのがわかると思います。

![pic1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/3421f671-0fc7-f9c6-4373-6d6c83f390fd.png)


CSS と同じような感じなので、理解しやすいと思います。

細かい説明は省略しますが、`DecorationRenderOptions` の以下の項目も CSS と同様なので理解しやすいかと思います。詳細は VSCode の API 仕様や、CSS の仕様をご確認ください。

* `cursor` : マウスカーソルの種類
* `fontStyle` : フォントのスタイル。イタリックとか
* `fontWeight` : フォントの線の太さ
* `letterSpacing` : 文字の間隔
* `opacity` : 透明度
* `textDecoration` : 下線とか上線とか。波線などにもできる

ちなみに CSS での対応する名前はキャメルケースではなく、ケバブケース（例えば `font-style`）になります。

## 範囲の自動拡張制御(rangeBehavior)

上記の拡張機能を実際に動かして、いろいろと試していると気づくかもしれませんが、色が変わっている `TODO` の直前直後に文字を追加すると、一瞬追加した文字も色が変わって（`TODO` と同じ色になって）いて、その後（ディレイタイム終了後）に色が消えていると思います。

これは、`DecorationRenderOptions` に設定する項目 `rangeBehavior` のデフォルト値が `OpenOpen` になっているためです。
この値は、装飾している文字範囲の直前と直後に文字を追加された場合、範囲を拡張するか(Open)、拡張しないか(Closed)かを指定するもので、前半が直前、後半が直後の指定です。

個人的には、ほとんどのケースでは範囲を拡張する必要はないと考えるので、基本は `ClosedClosed` を指定したほうが良いのではないかと思っています。

この `rangeBehavior` は意外と重要な項目ではないかと思います。特に後述の `before` や `after` を指定している場合におかしな値を定義していると、かなり違和感を感じると思います。

前述の拡張機能を以下のように修正すると、直前直後に文字を追加しても範囲が拡張されないようになります。

```typescript
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
    });
```

## ガーターアイコン(gutterIconPath)

ガーターアイコンを指定すると、文字装飾のある行の先頭（行番号などよりもさらに前）に、アイコンを表示することもできます。

例えば、以下のように修正してみます。

```typescript
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
        gutterIconPath: context.asAbsolutePath("images/todo.png"),
    });
```

また、プロジェクトフォルダ内に `images/todo.png` を追加して、拡張機能を動かしてみると以下のようになります。

![pic2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/71c83cd9-8987-2d27-6ae9-432345a89eca.png)

指定した行の先頭に、アイコン（赤地に白い「！」みたいなもの）が表示されていると思います。
サイズは 16x16 が推奨されているようです。

サイズが違う場合等には、`gutterIconSize` を指定して調整できるようです。詳細は API 仕様を確認してください。

## 範囲の前後にコンテンツの追加(before と after)

`before` と `after` を指定すると、指定した文字装飾の範囲の前後に、コンテンツを追加できます。
イメージとしては、CSS の `::before` 疑似要素や `::after` 疑似要素のような感じです。

追加できるのは文字列か、アイコンのどちらか一方になるようです。

例えば、前にアイコン、後ろにテキスト追加してみます。

```typescript
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
        before: {
            contentIconPath: context.asAbsolutePath("images/todo.png"),
        },
        after: {
            contentText: "[!]",
            color: "#0000ff",
            backgroundColor: "#000000",
        },
    });
```

![pic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/8a0e2d95-60b9-9faf-20a3-00e73d9d6d0b.png)

`before` や `after` の中にも `fontStyle` や `fontWeight` 等いくつかの項目が指定できます。詳細は API 仕様を参照ください。

## 文字の重ね合わせ表示

上記の `before` で、`contentText` と `width` を上手く使うことで、文字の重ね合わせの表示が可能です。

何を言っているのかわかりにくいと思うので、実際の例を見てください。
例えば、全角スペースを入力すると、薄い線で四角が表示されるようにしてみます。

```typescript
    const regex = /\u3000/g;
    const decorationType = vscode.window.createTextEditorDecorationType({
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
        before: {
            contentText: "\u2395",
            width: "0",
            color: "rgb(127,127,127)",
        },
    });
```

![pic4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/e3619bc1-686d-a524-e0d4-7dc8389dea05.png)

「おはよう」と「ございます」の間にある全角スペース（薄い四角）がわかると思います。

ちなみに、重ね合わせで表示している "&#x2395;" (U+2395) は、Unicode 的には「APL Functional Symbol Quad」という文字で、APL 言語の関数用の記号だそうです。

本当は、JIS にも登録されている "□" (U+25A1)が使いたかったのですが、この文字は日本語などのフォントでは幅広にデザインされていますが、英語などのフォントでは幅が狭く（正方形なので小さく）デザインされています。
Unicode の規格の [東アジアの文字幅](https://ja.wikipedia.org/wiki/%E6%9D%B1%E3%82%A2%E3%82%B8%E3%82%A2%E3%81%AE%E6%96%87%E5%AD%97%E5%B9%85) （East Asian Width） で、この文字は A (Ambiguous; 曖昧) という扱いになっており、表示される文脈で幅広かそうでないかが変わってくる文字なので、設定によっては上手く表示できないかもしれないということで、上記の U+2395 を使ってみました。Unicode 3.0 で追加された文字なので、大抵の環境では既に表示可能だろうと思っています。

もし、APL 記号が上手く表示されない環境があるようでした、違う文字に書き換えてください。

## ボーダー(border 等)

表示テキストに枠線を引くことができます。
CSS と同じように `border` でまとめて指定することも、`borderColor`, `borderStyle`, `borderWidth` でそれぞれ指定することもできます。

試しに `TODO` に白い枠線をつけてみます。

```typescript
    const regex = /\bTODO\b/g;
    const decorationType = vscode.window.createTextEditorDecorationType({
        border: "1px solid #ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
    });
```

![pic5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/efbbab6a-fb25-a2d6-99f4-149e3deca5a4.png)

ただ、いくつかよくわかっていない動作をしているんですよね。

* 同じボーダーを設定した範囲指定が隣り合っていると、それぞれの範囲に対してはボーダーが付かずに、それらの範囲全体に対してボーダーが表示されるみたい。
* `borderSpacing` も効いていないみたい？

詳しい人がいたら教えてほしい。

ちなみに、`borderRadius` は、CSS 同様に角丸のボーダーになります。

また、`border` とよく似たものに `outline` があります。CSS の `outline` と同じという説明になっています（ボーダーのさらに外側に、ボーダーのようなものを描画するはず）。

## その他の機能

### dark と light

`dark` と `light` という定義があります。

これは、各種定義を（主に色やアイコンだと思いますが）、ダークモードとライトモードで切り替えられるようにする定義です。
先ほどダークモードを前提に白い背景や枠線の定義にしていましたが、`dark` と `light` を使えばダークモードでは明るい色で表示して、ライトモードでは暗い色で表示する、といった定義ができます。

`dark` や `light` の中には `color` や `backgroundColor`、その他さまざまなものが指定可能です。詳細は API 仕様を参照してください。

### isWholeLine

`isWholeLine` を `true` にすると、範囲指定した行の全体（行頭から、行末を超えて画面右端まで）を装飾するようです。

```typescript
    const regex = /\bTODO\b/g;
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
        isWholeLine: true,
    });
```

![pic6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/a5d5369f-d74c-ae14-c109-8957476c1523.png)

`TODO` だけを範囲指定していますが、行全体が装飾されているのがわかると思います（変な矢印は改行の位置です）。


### overviewRulerLane と overviewRulerColor

`overviewRulerLane` を指定すると、スクロールバーあたりに、マーカーといったらいいのか、「色」が表示できます。
検索したときに、検索項目と一致した箇所に色がつくような感じですね。

```typescript
    const regex = /\bTODO\b/g;
    const decorationType = vscode.window.createTextEditorDecorationType({
        color: "#ee0000",
        backgroundColor: "#ffffff",
        rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
        overviewRulerLane: vscode.OverviewRulerLane.Right,
        overviewRulerColor: "#ffff00",
    });
```

実際の表示イメージは以下のような感じですが、ちょっとわかりにくいですかね。

![pic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/ca0f8752-f828-c44e-63c6-4866063aef0b.png)

画像は、スクロールバー内（右側にうっすら線が見えますよね）の右の方に黄色が表示されているところです。
右側(`Right`)以外にも、`Left`, `Center`, `Full` があるようです。

### ホバーメッセージ

文字装飾箇所に対して、マウスカーソルを合わせるとホバーメッセージを出すことができます。

これは `DecorationRenderOptions` ではなくて、`setDecorations()` するときに、指定した範囲と合わせて `DecorationOptions` に登録します。

```typescript
        const options: vscode.DecorationOptions[] = Array.from(
            text.matchAll(regex),
            match => ({
                range: new vscode.Range(
                    editor.document.positionAt(match.index!),
                    editor.document.positionAt(match.index! + match[0].length)),
                hoverMessage: new vscode.MarkdownString("後で **絶対** やること"),
            }));
        editor.setDecorations(decorationType, options);
```

この文字列には、マークダウン風の装飾ができるようです。
（直接文字列を指定する方法も使えるのですが、`MarkedString` のドキュメントを見ると deprecated のようです）

# 作った拡張機能について

というわけで、これらの機能を使った拡張機能を作ってみました。

## markdown-table-rainbow

作った拡張機能は単純で、マークダウンを書いているときに、テーブルの各カラムに色付けたらわかりやすくないだろうか、という思いがきっかけです。いわゆるレインボー系の拡張機能ですね（indent-rainbow とか Rainbow CSV みたいな）。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/1b7b4ca2-03ec-1be2-3c4d-64bb2b62093a.png)

実際に作った拡張機能は以下です。

https://marketplace.visualstudio.com/items?itemName=yoshi389111.markdown-table-rainbow

ソースも GitHub にあるので（上記の拡張機能のページから「Repository」でたどれると思います）、もしソースが見たい場合にはどうぞ。

その時に思ったことは、別の記事に記載しました。

https://qiita.com/yoshi389111/items/bd6435ef6830742e4d9d

特にどこでも宣伝はしていないので、誰も使っていないかと思ったら、現在すでに 10 回以上のダウンロードがされているようです（内 1 回分は私自身ですが）。

## visible-whitespace

上記の拡張機能を作って、さらに `setDecorations()` をお勉強していたら、もう一つ拡張機能を作りたくなりました。
機能的には、空白文字を見えるようにするというもので、既に似たようなものはたくさんあったので、本当に自分のお勉強用ですが。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/16371546-7e9b-29b1-a794-406e1b7d3067.png)

https://marketplace.visualstudio.com/items?itemName=yoshi389111.visible-whitespace

その時に思ったことは、別の記事に記載しました。

https://qiita.com/yoshi389111/items/0ae999c73acb2887424b

# 拡張機能の公開方法

拡張機能をマーケットプレイスで公開する方法も、各種記事があると思うので閉じておきます。主に将来の自分のためのメモです。

<details><summary>拡張機能の公開方法</summary>

大きく以下の手順を踏む必要があります。

* Azure DevOps のアカウントを作成し、アクセストークンを生成
* マーケットプレイスに publisher の登録
* マーケットプレイスに公開

https://code.visualstudio.com/api/working-with-extensions/publishing-extension#publishing-extensions

## Azure DevOps のアカウントを作成

[オフィシャルドキュメント](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token)を参考に。

* [Azure DevOps](https://go.microsoft.com/fwlink/?LinkId=307137) に行って、サインイン（なんかフォワードかリダイレクトされそうな URL だけどこれが正式なのかな？）
* 組織がまだなければ作る。左側の "New organization" から（詳細は、[組織の作成](https://learn.microsoft.com/azure/devops/organizations/accounts/create-organization)を参照）。作成済なら不要
* パーソナルアクセストークンを作る。右上の自分のアイコンの横の「人」のアイコン→「Personal Access Tokens」→「New Token」
    * Name はわかりやすければなんでもよいが、今回のトークンは VSCode の拡張機能の公開用なので、例えば「vscode-extensions」とか。あるいは、複数マシンで別トークンを使うのであればマシン名を入れる、など。
    * Scopes は「Custom defined」にして、「Show all scope」をクリックしてから、下の方にスクロースして「Marketplace」の「Manage」だけチェック
    * Create ボタンで作成
* 生成されたパーソナルアクセストークンはこのタイミングでしか確認できないので、忘れずにコピー。もし忘れたりコピーに失敗したら、古いトークンは削除して再度新しいトークンを作成してメモしなおす。
* パーソナルアクセストークンは有効期限があるので、有効期限が切れた場合にも同様の手順で新しいトークンを生成する必要がある。

## マーケットプレイスに publisher の登録

[マーケットプレイスの管理画面](https://marketplace.visualstudio.com/manage)で publisher を作成。
ここで作成した publisher 名はあとで使うのでメモ。私の場合にはいつものアカウント名にあわせて `yoshi389111` にした。

## マーケットプレイスに公開

まずは、`vsce` コマンドでログインする（グローバルでなくローカルにインストールしていれば `npx vsce ～` で）。
たぶん、この操作はトークンの有効期限が切れた場合や、別のマシンで操作するときにも必要になるはず。

```
vsce login <publisher 名>
```

パーソナルアクセストークンを聞かれるので、先ほどメモしたトークンを入力する。

`package.json` にも `publisher` を登録する。
他にも、`icon` とか、`repository` とか `license` とかも入れておいた方がよいかも。
必要なら `keywords` もかな。

```js
{
    // ...
    "icon": "assets/icon.png",
    "publisher": "yoshi389111",
    "repository": {
        "type": "git",
        "url": "https://github.com/yoshi389111/XXXXXXXXXXXXXXXXX"
    },
    "license": "MIT",
    // ...
}
```

詳細は、Extension Manifest を参照のこと。

https://code.visualstudio.com/api/references/extension-manifest

他にも `README.md` や `CHANGELOG.md` を修正する。バージョンアップ時には `package.json` のバージョン番号も修正が必要。

`package.json` と `package-lock.json` は以下のような感じでも更新できる（`CHANGELOG.md` と合わせてコミットしたいので、わざとコミットはしないようにしている。あとで手動でコミットが必要）。

```
npm version 0.0.2 --no-git-tag-version
```

バージョンを直接指定しないで、パッチバージョンを自動カウントアップさせたり

```
npm version patch --no-git-tag-version
```

マイナーバージョンを自動カウントアップさせたり

```
npm version minor --no-git-tag-version
```

したほうがミスが無くて良いかも。


マーケットプレイスに公開する際には、プロジェクトルート（`package.json` のあるところ）で、以下のコマンドを実行（ログインしている必要があるはず）。
バージョンアップ時も同じコマンドで OK。

```
vsce publish
```

メッセージがいろいろ出るけど、その中に拡張機能の公開 URL (`Extension URL` と書いてあるもの)と、各種情報が見える管理ページ（`Hub URL:` と書いてあるもの）が出力される（この管理ページは、前述のマーケットプレイスの管理画面からも行ける）。

実際に公開されるまでには数分かかるみたい。
その前に公開用 URL 等にアクセスしても `404 Not Found` になるので、のんびり待ってください。

</details>