VSCodeのシンタックスハイライトの作り方

はじめに

こんにちは。技術部の中山といいます。

社内の一部のプロジェクトでBaalというIDLが使われているのですが、 シンタックスハイライトがなくて書きにくいと言っている人がいたので、 VSCode用の拡張機能を作りました。

f:id:nakayama-daisuke:20200804143459p:plain

この記事では、VSCodeのシンタックスハイライトの作り方について紹介したいと思います。

なお、作った拡張機能はGitHubで公開しています。

注意

細かい文法については、ブログやQiitaなどに記事を書いている方がいるのでここでは説明しません。 ここでは、ドキュメントを読んだり実際に手を動かさないとわからなかったことについてだけ説明します。

2日くらいでブワーーーっと勉強して実装しただけなので、記述に誤りがあるかもしれません。

準備

code.visualstudio.com

ターミナルで

npm i -g yo
npm i -g vsce
yo code

と打って、 New Language Support を選んで質問に答えるとプロジェクトが作られます。 プロジェクトの中に syntaxes/xxx.tmLanguage.json というファイルがあるので、 ここにシンタックスハイライト用の定義を書いていきます。

xxx.tmLanguage.json には何を書くのか

xxx.tmLanguage.json には、TextMate文法というのをJSONにしたものを記述していきます。

TextMate文法とは

公式のドキュメントを見にいくと次のような記述があります。

https://macromates.com/manual/en/language_grammars

Language grammars are used to assign names to document elements such as keywords, comments, strings or similar. The purpose of this is to allow styling (syntax highlighting) and to make the text editor “smart” about which context the caret is in.

要するに、

ドキュメントの要素(キーワードとかコメントとか)に名前をつけるのに使う。そうすることでシンタックスハイライトとかできるようになる。

みたいなことが書いてあります。 つまり「要素に名前をつけるための規則の集まり」を記述するのがTextMate文法ということです。

JSONの中身

JSONには正規表現を使った規則をたくさん並べていきます。

たとえば、「identifier」という名前の、「\\b[[:upper:]][[:alnum:]]* という正規表現に一致する」という規則は次のように書きます。

"identifier": {
  "match": "\\b[[:upper:]][[:alnum:]]*"
}

正規表現エンジンには「鬼車」が使われてて、 JavaScriptの正規表現とちょっと違うので注意が必要ですが、 日本語のマニュアルがあるのでそこまで問題にはならないと思います。 https://macromates.com/manual/ja/regular_expressions

正規表現の制限

TextMateのドキュメントに次のような記述があります。

https://macromates.com/manual/en/language_grammars

Note that the regular expressions are matched against only a single line of the document at a time. That means it is not possible to use a pattern that matches multiple lines.

正規表現は行単位でのマッチしかしない(複数行にマッチする正規表現は書けない)みたいなことが書いてあります。

鬼車自体は複数行のマッチに対応していますが、 TextMate側の都合で行単位のマッチしかできないようになっています。

意図通りにいかない例

行単位でのマッチしかできないということは、 深く考えずに規則を書くと、 文法的には正しいのにハイライトがされない状況が生まれてしまうということになります。

f:id:nakayama-daisuke:20200804143724p:plain
BNFの一部

たとえば上の画像の四角で囲った部分にマッチさせる規則を考えてみます。

これは「entity」というキーワードの後ろにエンティティの名前が来るという文法を表しているので、 それを表す規則として次のようなものを書いたとします。

"entity": {
  "match": "\\b(entity)\\b([[:upper:]][[:alnum:]]*)"
}

こうすると、 entity の直後に改行なしで entity EntityName と書いた場合は問題なくハイライトできるけど、 改行を入れてしまうとハイライトされなくなってしまいます。

f:id:nakayama-daisuke:20200804143805p:plain
文法としては正しいのにハイライトされない

改行を処理するには

公式のドキュメントに次のような記述があります。

https://macromates.com/manual/en/language_grammars

In most situations it is possible to use the begin/end model to overcome this limitation.

「this limitation」は行単位のマッチしかできないことを指してるので、 要するに「複数行のマッチをしたいときはbegin, endを使えば大体なんとかなる」みたいなことが書いてあります。

begin, end

f:id:nakayama-daisuke:20200804143845p:plain
begin, endを使っている規則

begin, endは、おそらく

  • begin にマッチしなかったら失敗させる
  • テキストを1行ずつ見ていって、 patterns の中にマッチする規則があればその規則を適用する
  • end にマッチしたらマッチングを終わる

というような動作をします。 違ったらすみません。

begin の規則でマッチさせてから patterns の規則をマッチさせるまでの間に改行があっても問題ないので、 begin, endを使えば改行を扱えるようになるということになります。

さっきの例

先ほど「entity」の後ろに改行を入れるとハイライトがされなくなる例を見せましたが、 この問題を解決するには、

  • begin で「entity」というキーワードにマッチさせて、
  • patterns にエンティティの名前にマッチする規則を書く

というように規則を分ければ良いということになります。

f:id:nakayama-daisuke:20200804144823p:plain
規則を分割した

マッチした部分に名前をつける

TextMateについて

ドキュメントの要素(キーワードとかコメントとか)に名前をつけるのに使う

という説明をしましたが、どうやって名前をつけるのかというと、 "name" というキーを使います。

たとえば、「entity」というキーワードに storage.type.baal という名前をつけるには、

"entity": {
  "begin": "\\b(entity)\\b",
  "beginCaptures": {
    "1": {
      "name": "storage.type.baal"
    }
  },
  "end": ...

のようにします。

適切な名前をつけると、あとはVSCodeがいい感じにハイライトしてくれます。

f:id:nakayama-daisuke:20200804144902p:plain
「entity」に色がつく

どういう名前をつければ良いかは、公式のドキュメントの下の方に記述があります。 実際に使われてるtmLanguage.jsonも参考になると思います。 https://macromates.com/manual/en/language_grammars https://github.com/microsoft/vscode/blob/master/extensions/javascript/syntaxes/JavaScript.tmLanguage.json

その他やっておくと良さそうと思ったこと

ゆるい規則でマッチさせて、そのあとで厳密に規則に従ってるかを見るというのをやると、 「ここが文法違反してる」というのを示しつつ全体のハイライトは良い感じにしてくれるようになります。

f:id:nakayama-daisuke:20200804145450p:plain
beginにゆるい規則を書いて、beginCapturesで厳密な判定をする

たとえば上の画像のように規則を書くと、

  • 厳密に「identifier」の定義に従ってたら正しくハイライトする
  • 「identifier」の定義に従ってなかったら赤く表示する

ということができます。

f:id:nakayama-daisuke:20200804145547p:plain
「entityName」はエンティティの名前としては正しくない

デバッグの方法

VSCodeで▶️を押すとデバッグを開始できます。

f:id:nakayama-daisuke:20200804145626p:plain
VSCode

正しくハイライトされないとき

Ctrl+Shift+P(macなら⌘+Shift+P)を押して「Developer: Inspect Editor Tokens and Scopes」を選択すると、

f:id:nakayama-daisuke:20200804145703p:plain
Developer: Inspect Editor Tokens and Scopes

テキストのどこにどんな名前がついてるかが見えるようになります。

f:id:nakayama-daisuke:20200804150532p:plain
「textmate scopes」に、どんな名前がついているかが表示される

これを見れば意図通りにマッチングができているかがわかります。

textmate scopes

たとえば、

  • namespace全体にマッチする規則で、マッチした部分に meta.namespace.baal という名前をつける
  • 行コメントの中身にマッチする規則で、マッチした部分に comment.line.double-slash.baal という名前をつける

とすると、「namespaceの中のコメント」には

  • meta.namespace.baal
  • comment.line.double-slash.baal

の2つの名前が付いて、スタックトレースのような役割を果たしてくれます。

できる限り丁寧に名前をつけていくと、 意図通りに動かないときに素早く原因を特定できるようになります。

まとめ

文脈を見てハイライトしてくれるようにしたので、 文法エラーが容易に見つけられるようになりました。

issueにあるようにちゃんと作ってない部分もあるので、暇があればもう少し作り込みたいと思います。