全角スペース/改行/EOFとかを可視化するVSCode拡張機能を作ったよ

# はじめに

ふと思いついて [VSCode の拡張機能を 1 つ作り](https://qiita.com/yoshi389111/items/bd6435ef6830742e4d9d) 、そのために [お勉強](https://qiita.com/yoshi389111/items/8dca20680eb79f01cb2c) していたら、なにかもう一つ VSCode の拡張機能を作ってみたくなったので、作ってみました。

内容は、空白文字をみえるようにする拡張機能、です。

似たようなものはたくさんあると思うのですが、ちょっとお勉強のアウトプットを兼ねて、自分でも作ってみたくなったのです。

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

具体的には、以下のような機能を持っています。

* 水平タブ (U+0009) に、右向き矢印を重ね合わせで表示します（デフォルトは off）
* 改行文字として LF (U+000A) の位置に下向き矢印を表示します。
* 同様に CRLF (U+000D + U+000A) の位置に下向きから左に曲がる矢印を表示します。
* スペース (U+0020) に、中点を重ね合わせで表示します（デフォルトは off）
* No-Break Space (U+00A0; HTML の &nbsp;) に、細長い四角を重ね合わせで表示します。
* 全角スペース (U+3000) に、幅広の四角を重ね合わせで表示します。
* その他の空白文字 (U+180E, U+2000-U+200A, U+202F, U+205F, U+2800) にボーダーを付けます。また、ホバーメッセージで文字コード(U+2800 など）を表示します。
* ファイルの末尾に"[EOF]"を表示

一応、表示する文字や色は設定で変えられるようになっています。また、表示の on/off も設定できます。

水平タブとスペースについては、デフォルトでは off になっています。
これは、普通は標準機能の `editor.renderWhitespace` を使っているだろうというのと、私が作った拡張機能では、この標準機能の `all` での表示しかできないためです（標準機能を off にして、私の拡張機能の方を on にしてもらってももちろん良いですが）。

こちらも勢い余って、マーケットプレイスで公開しています。

【マーケットプレイス】

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

【リポジトリ】

https://github.com/yoshi389111/visible-whitespace

# 使い方

マーケットプレイスでダウンロードするか、VSCode で `visible-whitespace` で検索してインストールしてください。基本はそれで使えると思います。

対象のファイルタイプ（というか言語）は、デフォルトでは全ファイルタイプになっています。
ただし、全ファイルタイプはやりすぎだと思うので、定義で指定のファイルタイプのみ実行するようにできます。

## 対象となる言語リスト(visibleWhitespace.enabledLanguageIds)

空白文字を表示するファイルタイプ（というか言語）を指定してください。

例えば `[ "planintext", "markdown", "python", "typescript" ]` のように、VSCode の認識する言語種類を文字列の配列として指定してください。

未指定時、あるいは配列数が 0 の場合には、全ファイルタイプで有効だと判断します。

## その他の定義項目

各空白文字の on/off と、表示用の文字列と色を変更などができます。

* `visibleWhitespace.updateDelay`: 更新する前のディレイタイムをミリ秒単位で指定します。
* `visibleWhitespace.overlayColor`: 重ね合わせする文字の表示色を指定します。`#RRGGBB`, `rgb(R,G,B)` あるいは `rgba(R,G,B,A)` 形式で指定してください。
* `visibleWhitespace.htab.enable`: 水平タブを可視化するかどうかのスイッチです。 (default: false)
* `visibleWhitespace.htab.text`: 水平タブに重ね合わせするテキストです。デフォルトは右向き矢印。
* `visibleWhitespace.newLine.enable`: 改行を可視化するかのスイッチです。
* `visibleWhitespace.newLine.lf`: 改行コード LF を表すテキストです。デフォルトは下向き矢印。
* `visibleWhitespace.newLine.crLf`: 改行コード CRLF を表すテキストです。デフォルトは下から左に向く矢印。
* `visibleWhitespace.newLine.color`: 改行コードを表すテキストの色を指定します。`#RRGGBB`, `rgb(R,G,B)` あるいは `rgba(R,G,B,A)` 形式で指定してください。
* `visibleWhitespace.space.enable`: スペースを可視化するかどうかのスイッチです。 (default: false)
* `visibleWhitespace.space.text`: スペースに重ね合わせをするテキストです。デフォルトは中点。
* `visibleWhitespace.nbsp.enable`: No-Brake スペースを可視化するかどうかのスイッチです。
* `visibleWhitespace.nbsp.text`: No-Brake スペースに重ね合わせをするテキストです。デフォルトは細長い四角。
* `visibleWhitespace.widespace.enable`: 全角スペースを可視化するかどうかのスイッチです。
* `visibleWhitespace.widespace.text`: 全角スペースに重ね合わせをするテキストです。デフォルトは四角。
* `visibleWhitespace.other.enable`: その他の空白文字を可視化するかどうかのスイッチです。
* `visibleWhitespace.other.borderColor`: その他の空白文字の枠線の色です。`#RRGGBB`, `rgb(R,G,B)` あるいは `rgba(R,G,B,A)` 形式で指定してください。
* `visibleWhitespace.eof.enable`: EOF を可視化するかのスイッチです。
* `visibleWhitespace.eof.text`: EOF を表すテキストです。デフォルトは "EOF"。
* `visibleWhitespace.eof.color`: EOF を表すテキストの色を指定します。`#RRGGBB`, `rgb(R,G,B)` あるいは `rgba(R,G,B,A)` 形式で指定してください。

スイッチは基本、デフォルト true ですが、前述の通り水平タブとスペースはデフォルト false になっています。

# 技術的な話（というか、取り留めのないメモ）

VSCode の拡張機能の開発を作る話などは、Qiita 上にも他の箇所にも多数あるので、割愛します。

とりあえず、主に参考にした情報と、アウトプットです。

また、[別記事](https://qiita.com/yoshi389111/items/bd6435ef6830742e4d9d) と重複する内容は省略しました。

【オフィシャルな API ドキュメント】

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

【サンプルコード(decorator-sample)】

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

【その時の学習結果をアウトプットした物】

https://qiita.com/yoshi389111/items/8dca20680eb79f01cb2c


## 文字の重ね合わせ

API 仕様を見ていただけではわからなかったのですが、文字の装飾を使って文字の重ね合わせ表示ができるようです。

[別の記事](https://qiita.com/yoshi389111/items/8dca20680eb79f01cb2c)で記載した通り、例えば `before` にコンテンツを指定して、`width` に `"0"` を指定すると重ね合わせして表示されるようです。

```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)

全角スペースや、No-Brake スペース、タブ、スペースなどの重ね合わせはこの機能を使わないと実現できませんでした。

## 他の拡張機能との干渉

最初、改行コードは普通の `before` で表示していたの（つまり `widht` を `"0"` にしていなかったの）ですが、何か別の拡張機能と合わせて使うと動きがおかしくなりました。

具体的にはこの拡張機能だけだと、改行コードの before のコードの後ろにカーソルが行かないのですが、何かの拡張機能と合わせて使うと改行コードの矢印の前にカーソルが置けないで、矢印の後ろに移動してしまうというものです。

そのため、改行コードとついでに EOF についても、`width: "0"` を指定するようにしました。

具体的に何の拡張機能とぶつかっていたのかは、調査できていませんが……。

## APL 記号など

現在、全角スペースにはデフォルトでは四角を重ね合わせして表示しています。

重ね合わせで表示している四角 "&#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 記号が上手く表示されない環境があるようでした、違う文字で表示するように定義してください。

使用フォントにもよりますが、JIS 記号の四角"□"（うまく表示できるなら）や、マス記号"〼"、カタカナの"ロ"、漢字の"口"、あるいは"全"とか"白"を重ね合わせするとよいのではないかと思います（だんだん雑な対応になっていますが）。

同様に、No-Break Space についても APL 記号になっています。こちらも上手く表示できない場合には、何か適当な文字で表示するように定義してください。

他にも、東アジアの文字幅関連でおかしくなるものがあれば、同様に適当な文字で表示するように定義してください。

## 文字装飾のボーダー

上の方で「その他の空白文字」と書いたものは、朱色っぽいボーダーをつけていますが、[別の記事](https://qiita.com/yoshi389111/items/8dca20680eb79f01cb2c)で記載した通りこれらの文字が連続していると、ボーダーの範囲が結合してしまうみたい。

なんでだろう？

## before/after 要素が残る？

改行コードを削除する時に改行コードを表す矢印が一瞬残ること等がありました（ディレイタイム後に消える）。

当時は改行コードに `width: "0"` を指定していなかったのですが、複数行をまとめて削除すると、削除直後の一瞬は改行コードを表す矢印が複数個まとまって表示されて、その後消えるという動作をしました。

一応、私の環境では改行コードの表示幅を `width: "0"` にした後はあまり気になっていませんが、もし気になるようであれば、改行コードの表示を off にしてください。

## 改行コードの混在

改行コードが LF と CRLF が混在しているようなファイルを開く場合でも、VSCode はファイルを開いた時点でどちらかに補正しているようです。
なので、1 つのエディタ上で改行コードが混在するケースは基本的にはないのではないかと思います。

## activationEvents をすべて("*")にすると

当初この拡張機能では、ファイルタイプを絞らず、`"activationEvents": ["*"]` としているため、拡張機能を公開するとき（`vsce publish` コマンドを実行したとき）、毎回ワーニングが出ていました。

[ドキュメント](https://code.visualstudio.com/api/references/activation-events#onStartupFinished)をよくみると `onStartupFinished` というものがあるみたい。

> This activation event is emitted and interested extensions will be activated some time after VS Code starts up. This is similar to the `*` activation event, but it will not slow down VS Code startup. Currently, this event is emitted after all the `*` activated extensions have finished activating.

`*` だと起動時のパフォーマンスが落ちるけど、`onStartupFinished` だと起動時の処理が終わった後に呼び出されるのでパフォーマンスの問題が無くなるよ、ってことかな。

## BOM の有無の習得方法

本当は、BOM 付のファイルなら、先頭行に BOM のガーターアイコンをつけたいと思ったのですが、なぜか VSCode は文字コードや BOM の有無などは取得ができないみたいなんですよね。なんでだろう？

## EOF の範囲指定

ちなみに EOF の範囲には、ドキュメントの最後の文字のさらに後ろの 0 幅を指定しています。
以下のような感じ。こんな指定もできるんですね。

```typescript
            const eofPosition = editor.document.positionAt(text.length);
            const range = new vscode.Range(eofPosition, eofPosition);
```

## 色の指定方法

`settings.json` に色を指定してもらう項目がいくつかあります。
VSCode の定義の型には、色というものはないため、`string` で定義してもらうようにしています。

ただの `string` だとフリーフォーマットになってしまうので `package.json` の `pattern` に正規表現を指定して、色指定の形式チェックをしています。

今は以下の３パターンを許容しています。

* `#RRGGBB` 形式。RR/GG/BBは16進数2桁
* `rgb(R,G,B)` 形式。R/G/B は 0～255 の範囲。Aは指定不可
* `rgba(R,G,B,A)` 形式。R/G/B は 0～255 の範囲。Aは 0～1 までで、小数点形式も可（実際には `1.999` 等も指定可能になっているかも）

その他の形式は受け付けていません。
`package.json` の `pattern` に、正規表現を1つだけ指定する方式なので、あまり複雑なチェックはしにくいので。

でも `#RGB` とか `hsv()` とかでも指定できた方が良いのかな？