実践！VSCodeの自作シンタックスハイライト拡張機能開発

# 3行サマリ

- VSCode の拡張機能で、シンタックスハイライトを行う拡張機能を開発してみる
- 具体的に [PCSX2 のチートファイルの文法](https://forums.pcsx2.net/Thread-Sticky-Important-Patching-Notes-1-7-4546-Pnach-2-0)に色を付けてみる
- tmLanguage ファイルで指定する scope の汎用的な値を紹介する

作ってるもの

https://github.com/Take-Me1010/PCSX2-patch-language

↓ 拡張機能を入れるとこうなる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/591372ea-536e-734f-3256-10294816c0a7.png)

※本記事の内容だけでは、上記と同じになりません。是非試してみてください。

# 概要

シンタックスハイライトを行う拡張機能の仕組みって面白そうですよね。
実際、シンタックスハイライトを行う拡張機能について、最初の一歩はいくつかの記事で紹介されています。

https://qiita.com/OrukRed/items/03f0e38e0f8553ee35d2

https://qiita.com/takl/items/ba2f63db515f66585d1f

しかしながら、具体的に既存の文法が設定された言語に対して、一からシンタックスハイライトを行う方法はあまり言及されていません。
そこで本記事では、具体的に [PCSX2 のチートファイルの文法](https://forums.pcsx2.net/Thread-Sticky-Important-Patching-Notes-1-7-4546-Pnach-2-0)に基づいて、色を付ける拡張機能を開発する流れを通じて、tmLanguage ファイルの書き方や scope の値を紹介します。

# 前提

- json自体の文法等
- windows 10 (ただし、本記事にはOS依存な事象はないはず)
- node v20.10.0
- VSCode v1.92.1

なお、カラーテーマは Dracula Soft を前提として進めます。

https://draculatheme.com/visual-studio-code

:::note warn
本記事では、PS2のエミュレータのチート用のファイルを扱いますが、PCSX2のレポジトリよりコピペしたものを除いて、実際に使えるコードは一切掲載しません。
より詳細に言うと、`patch=...`という部分は、改変したりマスクしたりして掲載します。
:::

# 拡張機能作成

## 雛形作成

拡張機能開発では、`yo`を利用すれば雛形がほぼ出来上がります。

```bash
npm install -g vsce
npm install -g yo generator-code
yo code
```

今回は".pnach"ファイルを対象にして pcsx2-patch という言語モードを定義します。
質問の答え等は次の記事を参照:

https://qiita.com/OrukRed/items/03f0e38e0f8553ee35d2

大体次のような感じで package.json が出来上がるはず。

```package.json
{
    ...
    "contributes": {
        "languages": [
            {
                "id": "pcsx2-patch",
                "aliases": [
                    "pcsx2",
                    "pcsx2-patch"
                ],
                "extensions": [
                    ".pnach"
                ],
                "configuration": "./language-configuration.json"
            }
        ],
        "grammars": [
            {
                "language": "pcsx2-patch",
                "scopeName": "source.pcsx2",
                "path": "./syntaxes/pcsx2-patch.tmLanguage.json"
            }
        ],
    },
    ...
}
```

さて、シンタックスハイライトを行うためには package.json で指定されている次の二つのファイルを設定する必要があります:
- language-configuration.json
    - コメントの定義・かっこの対応等を定義するファイル
- syntaxes/pcsx2-patch.tmLanguage.json
    - キーワード等に色を付けるために、構文等を正規表現に基づいて scope を割り当てるファイル

これらのファイルを編集していくために、とりあえずは今回対象となるファイルのシンタックスを確認してみます。


## シンタックスの仕様

まず、今回の記事で紹介する PCSX2 のパッチファイルのシンタックスを紹介します。

参照: https://forums.pcsx2.net/Thread-Sticky-Important-Patching-Notes-1-7-4546-Pnach-2-0

1. 最初は gametitle に関する文を置いてもよい (恐らくPCSX2のv1.xでは必須だった)
1. author を設定できる
1. チートのセクション (階層構造) を `\` で区切って記述できる
1. `description=...`で説明を追加できる
1. `patch=...`という形で宣言する
1. `// ...`でコメント行

```8EFDBAEB.pnach
gametitle=Sarugecchu Millon Monkeys [SCPS-15115] (J)
author=TakeMe1010

// comment line
[Cheats\All opened\Chips]
description=色々やるチート
patch=1,EE,0011C545,word,FFFFFFFF
```

特殊なパッチとして、wide-screeen, No-Interlacing用途のものはパッチの名前が予約されている模様
```
[Widescreen 16:9]
gsaspectratio=16:9
patch=1,EE,21234253,extended,21234253

[No-Interlacing]
description=Attempts to disable interlaced offset rendering.
gsinterlacemode=1
//Remove Interlacing
patch=1,EE,65432178,extended,00000000]
description=Attempts to disable interlaced offset rendering.
```

以上が基本的な仕様です。
大体 ini ファイルのような形式？
(サルゲッチュミリオンモンキーズは、人生においてもっともやりこんだゲームの一つなのですが...新作かHDリマスターでないかなぁ...でないよなぁ...)

## language-configuration.json を編集する

このファイルには、行コメントの定義やかっこの対応をVSCodeに教える役割を持っています。
つまり、色を付ける部分には関係ない部分です。しかし、このファイルをしっかりと書いておけば、`ctrl+/`でコメントをトグルする機能等を利用できるようになります。

このjsonファイルには色々セクションがありますが、一番基礎的なものを3つ紹介します。
- comments
    - lineComment: 行コメントの頭に必要なものの指定。多くのプログラム言語では `//` or `#` あたり
    - blockComment: 複数行コメントの最初と最後に必要なものの指定。 C言語だと `/*` と `*/` など
- brackets
    - インデントの増減に関連するかっこの指定
- autoClosingPairs
    - 名前の通り、自動で閉じるかっこの指定。なんか色々設定があるらしい (このサブセクション末尾参照)

ここでは、`//`が行コメントで、チートのセクションに使われる`[]`のみを設定します。
```language-configuration.json
{
    "comments": {
        "lineComment": "//",
    },
    "brackets": [
        ["[", "]"],
    ],
    "autoClosingPairs": [
        ["[", "]"],
    ],
}
```

Pythonとか、お好きな言語の language-configuration.json を見てみると面白い。
詳しい公式ドキュメントもあるので、読んでみると設定の豊富さに驚くかもね:

https://code.visualstudio.com/api/language-extensions/language-configuration-guide

## tmLanguage ファイルの編集

さて、本記事の本題です。
tmLanguageファイルでは、キーワード等に色を付けるために、構文等を正規表現に基づいて scope を割り当てることを行います。いわゆる Tokenization です。

公式ドキュメント:

https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide

### scope とは

構文に対して、正規表現ベースで解析を行い、属性を割り当てるときの名前的なものです。
ちなみに拡張機能側で任意に設定できるそうな。
カラーテーマの拡張機能では、scope を参照して色を割り当てるような形になっています。
実際、Dracula のテーマを定義しているymlファイルでは、scope が多数割り当てられている様子が確認できます。

https://github.com/dracula/visual-studio-code/blob/main/src/dracula.yml

実際に、どのような scope が割り当てられているのかは、コマンドでチェック可能です。
`Developer: Inspect Editor Tokens and Scopes`コマンドで、下記のようにエディタ上で scope を確認可能です:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/623a0e19-39ff-e337-3f4a-8226be688ec0.png)

`textmate scopes` と書かれているものが該当します。
カラーテーマの拡張機能は、この値を見て色を割り当てます。

### コメントを色付けしてみる

それではまず最初に、コメントを色付けしてみます。
コメントに対応する scope を探して、割り当てることができればよいですね。
前サブセクションで、適当なプログラムのソースコードでコメントの scope を確認して、その値を割りあててもよいですが、ここではカラーテーマの拡張機能の scope を眺めてみます。

では、Dracula のコメントの定義部分を見てみます。
[dracula.yml#L591](https://github.com/dracula/visual-studio-code/blob/ca5a2c4581978e343e0539dee79c00de316871aa/src/dracula.yml#L591) あたり参照。

```dracula.yml
  - name: Comments
    scope:
    - comment
    - punctuation.definition.comment
    - unused.comment
    - wildcard.comment
    settings:
      foreground: *COMMENT
```

ここの scope で指定されている scope を指定すれば良さそうです。
今回はもっともシンプルそうな comment を割り当てることにします。
ただし、様々な拡張機能を眺めていると、慣例として scope の末尾に言語の名前を入れるようですので、今回は `comment.pcsx2-patch` で割り当てます。

:::note info
repositoryの中で定義すると、includeで読み込むことができます。
基本的に、repositoryに登録していく形が多いようです。
:::


```json:syntaxes/pcsx2-patch.tmLanguage.json
{
	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
	"name": "pcsx2",
	"patterns": [
		{
			"include": "#comment"
		}
	],
	"repository": {
        "comment": {
			"patterns": [
				{
					"name": "comment.pcsx2-patch",
                    "match": "\\/\\/.*$"
				}
			]
		}
    }
}
 ```

ここで重要なのは match の設定です。
ここは行コメントなので、`//`で始まり、行末で終わるということを正規表現で指定する必要があります。

:::note warn
ややこしいことに、`/`という文字はjsの仕様ではエスケープする必要があります。なので`\/`になる...かと思いきや、これでは特殊文字になってしまうのです。したがって`\`を更にエスケープするために`\\/`という形で指定します。
:::

これだけで、コメントとして scope が割り当てられるので、 F5 でデバッグしてみると確かに色がコメントになっていることが確認できます。

### gametitle文を色付けしてみる

次に、`gametitle=...`の部分を色付けしてみます。
この文は`gametitle`と`=`と右辺の値 (ゲーム名) から構成されています。
(細かく言えば、ゲーム名は`[NTSC-J] (SCPS-15115)`のような型番みたいな値も含まれますが、ここでは区別しないことにします)

キーワードとなる `gametitle` と `=` 及び右辺のゲーム名は色を分けたいので、正規表現でキャプチャして scope を分けて定義してみます。

```json:syntaxes/pcsx2-patch.tmLanguage.json
{
	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
	"name": "pcsx2",
	"patterns": [
		{
			"include": "#comment"
		},
        {
            "include": "#game_statement"
        }
	],
	"repository": {
        "game_statement": {
			"match": "^(gametitle)(=)(.*?)$",
			"captures": {
				"1": {
					"name": "variable.other.predefined"
				},
				"2": {
					"name": "punctuation.separator.key-value"
				},
				"3": {
					"name": "support.game-title"
				}
			}
		},
        "comment": {
			"patterns": [
				{
					"name": "comment.pcsx2-patch",
                    "match": "\\/\\/.*$"
				}
			]
		}
    }
}
```

少し解説。
ここでは正規表現で match した (キャプチャした) グループに応じて、順番に scope を定義してあげます。
`=`には punctuation や separator に該当する scope を割り当ててみます。 ([dracula.yml#L768](https://github.com/dracula/visual-studio-code/blob/ca5a2c4581978e343e0539dee79c00de316871aa/src/dracula.yml#L768)付近参照)

```json
		"game_statement": {
			"match": "^(gametitle)(=)(.*?)$",
			"captures": {
				"1": {
					"name": "variable.other.predefined"
				},
				"2": {
					"name": "punctuation.separator.key-value"
				},
				"3": {
					"name": "support.game-title"
				}
			}
		},
```

:::note info
scope は部分的に一致していたらそれでカラーテーマが適用されます。
例えば、`support.game-title`は`support`に一致するので、Draculaではカラーが適用されます。 ([該当箇所](https://github.com/dracula/visual-studio-code/blob/ca5a2c4581978e343e0539dee79c00de316871aa/src/dracula.yml#L746))
:::

### patch文を色付けしてみる

次に`patch=...`の部分に色をつけてみます。

```json:syntaxes/pcsx2-patch.tmLanguage.json
{
	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
	"name": "pcsx2",
	"patterns": [
		{
			"include": "#statement"
		},
		{
			"include": "#comment"
		}
	],
	"repository": {
		"statement": {
			"patterns": [
				{
					"include": "#game_statement"
				},
				{
					"include": "#patch_statement"
				}
			]
		},
		"game_statement": {
			"match": "^(gametitle)(=)(.*?)$",
			"captures": {
				"1": {
					"name": "variable.other.predefined"
				},
				"2": {
					"name": "punctuation.separator.key-value"
				},
				"3": {
					"name": "support.game-title"
				}
			}
		},
		"patch_statement": {
			"match": "^(.*?)(=)(.*?)$",
			"captures": {
				"1": {
					"patterns": [
						{
							"include": "#patch_keys"
						}
					]
				},
				"2": {
					"name": "punctuation.separator.key-value"
				},
				"3": {
					"name": "string.unquoted"
				}
			}
		},
		"patch_keys": {
			"match": "comment|description|patch|author|gsaspectratio|gsinterlacemode",
			"name": "support.type.property-name"
		},
		"comment": {
			"patterns": [
				{
					"name": "comment.pcsx2-patch",
					"match": "\\/\\/.*$"
				}
			]
		}
	},
	"scopeName": "source.pcsx2"
}
```

`patch=...`以外にもauthorとかdescriptionもあるので、まとめて定義します。
`"match": "^(.*?)(=)(.*?)$"`でキャプチャして、順番に定義していきます。
右辺の値については、[dracula.yml#L971](https://github.com/dracula/visual-studio-code/blob/ca5a2c4581978e343e0539dee79c00de316871aa/src/dracula.yml#L971)あたりが参考になると思います。

### ヘッダに色を付ける

最後に`[Hoge\Fuga\Piyo]`の形で定義される、チートのセクションヘッダに色を付けます。
いくらでも階層構造を作ることができるので、再帰的な定義が必要に見えますが、実はとても簡単です。

```json:syntaxes/pcsx2-patch.tmLanguage.json
{
	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
	"name": "pcsx2",
	"patterns": [
		{
			"include": "#header"
		},
		{
			"include": "#statement"
		},
		{
			"include": "#comment"
		}
	],
	"repository": {
		"header": {
			"match": "^(\\[)([^\\]]+)(\\])$",
			"captures": {
				"1": {
					"name": "punctuation.section.scope.begin"
				},
				"2": {
					"patterns": [
						{
							"include": "#reserved_headers"
						},
						{
							"match": "([^\\\\]+)",
							"name": "entity.name.function"
						},
						{
							"match": "(\\\\)",
							"name": "punctuation.separator.key-value"
						}
					]
				},
				"3": {
					"name": "punctuation.section.scope.end"
				}
			}
		},
		"reserved_headers": {
			"match": "No-Interlacing|Widescreen 16:9",
			"name": "variable.other.predefined"
		},
		"statement": {
			"patterns": [
				{
					"include": "#game_statement"
				},
				{
					"include": "#patch_statement"
				}
			]
		},
        …
	},
	"scopeName": "source.pcsx2"
}
```

解説。
単に`[...]`でマッチさせて、そのマッチした中で `/` かそうじゃないかで scope を設定し分けるだけで済みます。

```json
        "header": {
			"match": "^(\\[)([^\\]]+)(\\])$",
			"captures": {
				"1": {
					"name": "punctuation.section.scope.begin"
				},
				"2": {
					"patterns": [
						{
							"include": "#reserved_headers"
						},
						{
							"match": "[^\\\\]+",
							"name": "entity.name.function"
						},
						{
							"match": "\\\\",
							"name": "punctuation.separator.key-value"
						}
					]
				},
				"3": {
					"name": "punctuation.section.scope.end"
				}
			}
		},
		"reserved_headers": {
			"match": "No-Interlacing|Widescreen 16:9",
			"name": "variable.other.predefined"
		},
```

上から順番にマッチすると思われるので、先に予約されているセクション名を確認しています。

これで一通り、仕様を満たすシンタックスハイライトができました！

## 拡張機能をデバッグする

拡張機能の開発は、基本的にF5一発でデバッグできる設定になっています。
デバッグが効いたウィンドウで実際に.pnachファイルを作ると、どのように表示されるかを確認ができます。
また、お使いのカラーテーマによっては、本記事で利用した scope に対応していないかもしれません。
ご利用のカラーテーマの拡張機能のコード (リポジトリ) を見て、調整してみると面白いかもしれません。

## まとめ

本記事では、PCSX2のチートファイルを題材にした、シンタックスハイライト用の拡張機能の開発を行いました。
特に scope について詳細に言及したので、これでだれでもシンタックスハイライトを作れるはずです。


## 今後の展望

- [こちらの記事](https://qiita.com/takl/items/ba2f63db515f66585d1f#tree-data-provider-%E3%82%92%E4%BD%9C%E3%82%8B)を参考に、tree-data-viewを動的に作成する

それでは楽しい拡張機能開発を！