シンタックスハイライト ガイド
構文ハイライトは、Visual Studio Code エディタに表示されるソースコードの色とスタイルを決定します。JavaScriptのifやforといったキーワードを、文字列、コメント、変数名とは異なる色で表示する役割を担っています。
構文ハイライトには2つの構成要素があります
- トークン化 (Tokenization): テキストをトークンのリストに分割すること
- テーマ設定 (Theming): テーマやユーザー設定を使用して、トークンを特定のカラーやスタイルに割り当てること
詳細に入る前に、まずスコープインスペクターツールを使って、ソースファイル内にどのようなトークンが存在し、どのテーマ規則と一致しているかを確認してみることをお勧めします。セマンティックトークンと構文トークンの両方を確認するには、TypeScriptファイルで組み込みテーマ(例: Dark+)を使用してください。
トークン化
テキストのトークン化とは、テキストをセグメントに分割し、各セグメントをトークンタイプに分類する作業です。
VS Codeのトークン化エンジンは、TextMate文法 (TextMate grammars)によって駆動しています。TextMate文法は正規表現の構造化された集合であり、plist(XML)またはJSONファイルとして記述されます。VS Code拡張機能は、grammars貢献ポイントを通じて文法を提供できます。
TextMateトークン化エンジンはレンダラーと同じプロセスで実行され、ユーザーの入力に応じてトークンが更新されます。トークンは構文ハイライトだけでなく、ソースコードをコメント、文字列、正規表現などの領域に分類するためにも使用されます。
リリース1.43以降、VS Codeでは拡張機能がセマンティックトークンプロバイダー (Semantic Token Provider)を通じてトークン化を提供することも可能になりました。セマンティックプロバイダーは通常、ソースファイルをより深く理解し、プロジェクトのコンテキスト内でシンボルを解決できる言語サーバーによって実装されます。例えば、定数変数の名前は、宣言箇所だけでなくプロジェクト全体で定数用のハイライトを使用してレンダリングできます。
セマンティックトークンに基づくハイライトは、TextMateベースの構文ハイライトへの追加要素とみなされます。セマンティックハイライトは構文ハイライトの上に重ねられます。言語サーバーがプロジェクトを読み込み分析するまでには時間がかかる場合があるため、セマンティックトークンハイライトはわずかな遅延の後に表示されることがあります。
本記事ではTextMateベースのトークン化に焦点を当てます。セマンティックトークン化およびテーマ設定については、セマンティックハイライトガイドで説明されています。
TextMate文法
VS Codeは構文トークン化エンジンとしてTextMate文法を使用しています。これはTextMateエディタ向けに発明されたもので、オープンソースコミュニティによって作成・維持されている膨大な言語バンドルの存在により、他の多くのエディタやIDEでも採用されています。
TextMate文法はOniguruma正規表現に依存しており、通常はplistまたはJSONとして記述されます。TextMate文法の優れた入門資料はこちらにあります。また、既存のTextMate文法を確認することで、その仕組みを詳しく学ぶことができます。
TextMateトークンとスコープ
トークンとは、同一のプログラム要素の一部である1つ以上の文字です。トークンの例には、+や*のような演算子、myVarのような変数名、"my string"のような文字列が含まれます。
各トークンには、そのトークンのコンテキストを定義するスコープが関連付けられています。スコープは、現在のトークンのコンテキストを指定するドット区切りの識別子リストです。例えば、JavaScriptの+演算子は、keyword.operator.arithmetic.jsというスコープを持っています。
テーマはスコープをカラーやスタイルにマッピングして構文ハイライトを実現します。TextMateは、多くのテーマがターゲットとする共通スコープのリストを提供しています。文法を可能な限り広くサポートさせるために、新しいスコープを定義するのではなく、既存のスコープをベースにすることをお勧めします。
スコープはネストするため、各トークンは親スコープのリストにも関連付けられます。以下の例では、スコープインスペクターを使用して、単純なJavaScript関数内の+演算子のスコープ階層を表示しています。最も具体的なスコープが一番上にリストされ、より一般的な親スコープがその下にリストされます。

親スコープの情報もテーマ設定に使用されます。テーマがあるスコープをターゲットにすると、そのテーマが個別のスコープに対してより具体的な配色を提供していない限り、その親スコープを持つすべてのトークンがその色で表示されます。
括弧マッチングスコープの設定
一部の言語には、見た目は括弧に似ていても括弧マッチングに参加させるべきではないトークンが含まれています。
括弧マッチングの動作を設定するには2つのプロパティがあります
balancedBracketScopes: どのスコープが括弧マッチングに参加するかを定義します。デフォルトでは、すべてのスコープが含まれます。unbalancedBracketScopes: 括弧マッチングから除外すべきスコープを定義します。
{
"unbalancedBracketScopes": ["meta.scope.case-pattern.shell"]
}
基本的な文法の提供
VS CodeはJSON形式のTextMate文法をサポートしています。これらはgrammars貢献ポイントを通じて提供されます。
各文法の貢献設定では、その文法が適用される言語の識別子、文法のトークンのトップレベルスコープ名、および文法ファイルへの相対パスを指定します。以下の例は、架空のabc言語に対する文法の貢献設定を示しています。
{
"contributes": {
"languages": [
{
"id": "abc",
"extensions": [".abc"]
}
],
"grammars": [
{
"language": "abc",
"scopeName": "source.abc",
"path": "./syntaxes/abc.tmGrammar.json"
}
]
}
}
文法ファイル自体はトップレベルのルールで構成されます。これは通常、プログラムのトップレベル要素をリストするpatternsセクションと、各要素を定義するrepositoryに分割されます。文法内の他のルールは、{ "include": "#id" }を使用してrepository内の要素を参照できます。
abc文法の例では、文字a、b、cをキーワードとしてマークし、括弧の入れ子を式として扱います。
{
"scopeName": "source.abc",
"patterns": [{ "include": "#expression" }],
"repository": {
"expression": {
"patterns": [{ "include": "#letter" }, { "include": "#paren-expression" }]
},
"letter": {
"match": "a|b|c",
"name": "keyword.letter"
},
"paren-expression": {
"begin": "\\(",
"end": "\\)",
"beginCaptures": {
"0": { "name": "punctuation.paren.open" }
},
"endCaptures": {
"0": { "name": "punctuation.paren.close" }
},
"name": "expression.group",
"patterns": [{ "include": "#expression" }]
}
}
}
文法エンジンは、ドキュメント内のすべてのテキストに対してexpressionルールを順次適用しようとします。以下のような単純なプログラムの場合:
a
(
b
)
x
(
(
c
xyz
)
)
(
a
この文法例は、以下のスコープを生成します(最も具体的なスコープから順に左から右へリストされています)。
a keyword.letter, source.abc
( punctuation.paren.open, expression.group, source.abc
b keyword.letter, expression.group, source.abc
) punctuation.paren.close, expression.group, source.abc
x source.abc
( punctuation.paren.open, expression.group, source.abc
( punctuation.paren.open, expression.group, expression.group, source.abc
c keyword.letter, expression.group, expression.group, source.abc
xyz expression.group, expression.group, source.abc
) punctuation.paren.close, expression.group, expression.group, source.abc
) punctuation.paren.close, expression.group, source.abc
( punctuation.paren.open, expression.group, source.abc
a keyword.letter, expression.group, source.abc
文字列xyzのように、ルールに一致しないテキストは現在のスコープに含まれることに注意してください。ファイルの最後にある最後の括弧は、endルールが一致しなかった場合でも、end-of-documentがendルールより前に見つかったため、expression.groupの一部となります。
埋め込み言語
HTML内のCSSスタイルブロックのように、親言語内に埋め込み言語が含まれる文法の場合、embeddedLanguages貢献ポイントを使用して、埋め込み言語を親言語とは別のものとして扱うようVS Codeに指示できます。これにより、埋め込み言語内でも括弧マッチングやコメントアウトなどの基本的な言語機能が期待通りに動作するようになります。
embeddedLanguages貢献ポイントは、埋め込み言語内のスコープをトップレベルの言語スコープにマッピングします。以下の例では、meta.embedded.block.javascriptスコープ内のすべてのトークンがJavaScriptコンテンツとして扱われます。
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/abc.tmLanguage.json",
"scopeName": "source.abc",
"embeddedLanguages": {
"meta.embedded.block.javascript": "javascript"
}
}
]
}
}
これで、meta.embedded.block.javascriptとマークされたトークンセット内でコードをコメントアウトしたりスニペットを呼び出したりすると、正しい//のJavaScript形式のコメントと、正しいJavaScriptスニペットが適用されます。
新しい文法拡張機能の開発
新しい文法拡張機能を素早く作成するには、VS CodeのYeomanテンプレートを使用してyo codeを実行し、New Languageオプションを選択します。

Yeomanがいくつかの基本的な質問を行い、新しい拡張機能の雛形を作成します。新しい文法を作成するための重要な質問は以下の通りです。
Language id- 言語の一意の識別子。Language name- 言語の人間が読める名前。Scope names- 文法のルートTextMateスコープ名。

ジェネレーターは、新しい言語の定義と、その言語の新しい文法の両方を定義したいと想定しています。既存の言語のために文法を作成している場合は、ターゲット言語の情報に合わせてこれらを入力し、生成されたpackage.json内のlanguages貢献ポイントを忘れずに削除してください。
すべての質問に答えると、Yeomanは以下のような構造を持つ新しい拡張機能を作成します。

繰り返しますが、VS Codeがすでに認識している言語に文法を追加する場合は、生成されたpackage.json内のlanguages貢献ポイントを必ず削除してください。
既存のTextMate文法の変換
yo codeは、既存のTextMate文法をVS Code拡張機能に変換する際にも役立ちます。繰り返しになりますが、yo codeを実行してLanguage extensionを選択してください。既存の文法ファイルを尋ねられたら、.tmLanguageまたは.jsonのTextMate文法ファイルへのフルパスを指定します。

YAMLを使用した文法の記述
文法が複雑になるにつれ、JSONとして理解し維持することが困難になる場合があります。複雑な正規表現を記述したり、文法の各部分を説明するためのコメントを追加する必要がある場合は、YAMLを使用して文法を定義することを検討してください。
YAML文法はJSONベースの文法と全く同じ構造を持っていますが、複数行文字列やコメントなどの機能とともに、YAMLのより簡潔な構文を利用できます。

VS CodeはJSON文法しか読み込めないため、YAMLベースの文法はJSONに変換する必要があります。js-yamlパッケージとコマンドラインツールを使用すれば、簡単に変換できます。
# Install js-yaml as a development only dependency in your extension
$ npm install js-yaml --save-dev
# Use the command-line tool to convert the yaml grammar to json
$ npx js-yaml syntaxes/abc.tmLanguage.yaml > syntaxes/abc.tmLanguage.json
インジェクション文法 (Injection grammars)
インジェクション文法を使用すると、既存の文法を拡張できます。インジェクション文法とは、既存の文法内の特定のスコープに注入される通常のTextMate文法のことです。インジェクション文法の適用例:
- コメント内の
TODOのようなキーワードのハイライト。 - 既存の文法により具体的なスコープ情報を追加する。
- Markdownのフェンス付きコードブロックに新しい言語のハイライトを追加する。
基本的なインジェクション文法の作成
インジェクション文法は、通常の文法と同様にpackage.jsonを通じて提供されます。ただし、languageを指定する代わりに、インジェクション文法ではinjectToを使用して、文法を注入するターゲット言語スコープのリストを指定します。
この例では、JavaScriptコメント内のTODOをキーワードとしてハイライトする単純なインジェクション文法を作成します。JavaScriptファイルにインジェクション文法を適用するには、injectToでsource.jsターゲット言語スコープを使用します。
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "todo-comment.injection",
"injectTo": ["source.js"]
}
]
}
}
文法自体は、トップレベルのinjectionSelectorエントリを除けば標準的なTextMate文法です。injectionSelectorは、注入された文法をどのスコープに適用するかを指定するスコープセレクターです。この例では、すべての//コメント内の単語TODOをハイライトしたいため、スコープインスペクターを使用してJavaScriptのダブルスラッシュコメントのスコープがcomment.line.double-slashであることを確認し、インジェクションセレクターをL:comment.line.double-slashとします。
{
"scopeName": "todo-comment.injection",
"injectionSelector": "L:comment.line.double-slash",
"patterns": [
{
"include": "#todo-keyword"
}
],
"repository": {
"todo-keyword": {
"match": "TODO",
"name": "keyword.todo"
}
}
}
インジェクションセレクターのL:は、インジェクションが既存の文法ルールの左側(前)に追加されることを意味します。つまり、インジェクションされた文法のルールは、既存のすべての文法ルールよりも先に適用されます。
埋め込み言語
インジェクション文法は、親文法に埋め込み言語を追加することもできます。通常の文法と同様に、インジェクション文法はembeddedLanguagesを使用して、埋め込み言語のスコープをトップレベルの言語スコープにマッピングできます。
例えば、JavaScriptの文字列内にSQLクエリをハイライトする拡張機能は、embeddedLanguagesを使用して、meta.embedded.inline.sqlとマークされた文字列内のすべてのトークンが、括弧マッチングやスニペット選択といった基本的な言語機能のためにSQLとして扱われるようにします。
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "sql-string.injection",
"injectTo": ["source.js"],
"embeddedLanguages": {
"meta.embedded.inline.sql": "sql"
}
}
]
}
}
トークンタイプと埋め込み言語
インジェクション言語や埋め込み言語には、もう一つ複雑な点があります。デフォルトでは、VS Codeは文字列内のすべてのトークンを文字列コンテンツとして、コメント内のすべてのトークンをコメントコンテンツとして扱います。括弧マッチングや自動閉じペアのような機能は文字列やコメント内では無効化されるため、埋め込み言語が文字列やコメント内に出現すると、それらの機能も埋め込み言語内で無効化されてしまいます。
この動作をオーバーライドするには、meta.embedded.*スコープを使用して、VS Codeによる文字列やコメントコンテンツとしてのトークンのマーク付けをリセットできます。埋め込み言語をmeta.embedded.*スコープでラップし、VS Codeが埋め込み言語を適切に処理するようにすることをお勧めします。
もし文法にmeta.embedded.*スコープを追加できない場合は、代わりに文法の貢献ポイントでtokenTypesを使用して、特定のスコープをコンテンツモードにマッピングすることができます。以下のtokenTypesセクションでは、my.sql.template.stringスコープ内のコンテンツがソースコードとして扱われることを保証します。
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "sql-string.injection",
"injectTo": ["source.js"],
"embeddedLanguages": {
"my.sql.template.string": "sql"
},
"tokenTypes": {
"my.sql.template.string": "other"
}
}
]
}
}
テーマ設定
テーマ設定は、トークンに色やスタイルを割り当てる作業です。テーマ規則はカラーテーマで指定されますが、ユーザーはユーザー設定でテーマ規則をカスタマイズすることもできます。
TextMateのテーマ規則はtokenColorsで定義され、通常のTextMateテーマと同じ構文を持ちます。各ルールは、TextMateスコープセレクターと、それに対応するカラーおよびスタイルを定義します。
トークンのカラーとスタイルを評価する際、現在のトークンのスコープがルールのセレクターと照合され、各スタイルプロパティ(フォアグラウンド、太字、イタリック、下線)に対して最も具体的なルールが検索されます。
カラーテーマガイドにはカラーテーマの作成方法が記述されています。セマンティックトークンのテーマ設定については、セマンティックハイライトガイドで説明されています。
スコープインスペクター
VS Codeの組み込みスコープインスペクターツールは、文法やセマンティックトークンのデバッグに役立ちます。ファイル内の現在位置におけるトークンのスコープとセマンティックトークンを表示し、そのトークンにどのテーマ規則が適用されているかに関するメタデータを表示します。
コマンドパレットからDeveloper: Inspect Editor Tokens and Scopesコマンドを実行するか、キーバインディングを作成してスコープインスペクターを起動します。
{
"key": "cmd+alt+shift+i",
"command": "editor.action.inspectTMScopes"
}

スコープインスペクターには以下の情報が表示されます。
- 現在のトークン。
- トークンに関するメタデータと、計算された外観に関する情報。埋め込み言語を扱う場合、ここで重要な項目は
languageとtoken typeです。 - セマンティックトークンセクションは、現在の言語でセマンティックトークンプロバイダーが利用可能であり、現在のテーマがセマンティックハイライトをサポートしている場合に表示されます。現在のセマンティックトークンのタイプと修飾子、およびそれらのタイプと修飾子に一致するテーマ規則が表示されます。
- TextMateセクションには、現在のTextMateトークンのスコープリストが表示され、最も具体的なスコープが一番上に表示されます。また、スコープに一致する最も具体的なテーマ規則も表示されます。これはトークンの現在のスタイルを決定しているテーマ規則のみを表示するものであり、上書きされたルールは表示されません。セマンティックトークンが存在する場合、セマンティックトークンに一致するルールと異なる場合のみテーマ規則が表示されます。