シンタックスハイライトガイド
構文ハイライトは、Visual Studio Code エディターに表示されるソースコードの色とスタイルを決定します。JavaScript における `if` や `for` のようなキーワードを、文字列、コメント、変数名とは異なる色で表示する役割を担います。
構文ハイライトには、2つのコンポーネントがあります
詳細に入る前に、スコープインスペクターツールを試して、ソースファイルにどのようなトークンが存在し、どのテーマルールに一致するかを調べてみるのが良いでしょう。セマンティックトークンと構文トークンの両方を確認するには、TypeScript ファイルで組み込みテーマ(例: Dark+)を使用してください。
トークン化
テキストのトークン化とは、テキストをセグメントに分割し、各セグメントをトークンタイプで分類することです。
VS Code のトークン化エンジンは、TextMate 文法を基盤としています。TextMate 文法は、正規表現の構造化された集合であり、plist (XML) または JSON ファイルとして記述されます。VS Code 拡張機能は、`grammars` 寄与ポイントを通じて文法を寄与できます。
TextMate トークン化エンジンはレンダラーと同じプロセスで実行され、ユーザーが入力するとトークンが更新されます。トークンは構文ハイライトに使用されるだけでなく、ソースコードをコメント、文字列、正規表現の領域に分類するためにも使用されます。
リリース 1.43 から、VS Code は拡張機能がセマンティックトークンプロバイダーを通じてトークン化を提供することも可能にします。セマンティックプロバイダーは通常、ソースファイルについてより深く理解し、プロジェクトのコンテキストでシンボルを解決できる言語サーバーによって実装されます。例えば、定数変数の名前は、宣言された場所だけでなく、プロジェクト全体で定数ハイライトを使用してレンダリングできます。
セマンティックトークンに基づくハイライトは、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 関数内の `+` 演算子のスコープ階層を示しています。最も具体的なスコープが一番上にリストされ、より一般的な親スコープがその下にリストされています
親スコープ情報はテーマ設定にも使用されます。テーマが特定のスコープを対象とする場合、テーマが個々のスコープに対してより具体的な色付けを提供しない限り、その親スコープを持つすべてのトークンは色付けされます。
基本的な文法の寄与
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` ルールが一致しなくても `expression.group` の一部です。これは、`end` ルールが見つかる前に `end-of-document` が見つかったためです。
埋め込み言語
文法に、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
インジェクショングラマー
インジェクショングラマーは、既存の文法を拡張することを可能にします。インジェクショングラマーは、既存の文法内の特定のスコープに注入される通常の 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"
}
}
]
}
}
トークンタイプと埋め込み言語
インジェクション言語の埋め込み言語には、もう1つ複雑な点があります。デフォルトでは、VS Code は文字列内のすべてのトークンを文字列コンテンツとして扱い、コメント内のすべてのトークンをトークンコンテンツとして扱います。括弧の照合や自動閉じペアなどの機能は文字列やコメント内では無効になるため、埋め込み言語が文字列またはコメント内に現れる場合、これらの機能も埋め込み言語で無効になります。
この動作を上書きするには、`meta.embedded.*` スコープを使用して、VS Code がトークンを文字列またはコメントコンテンツとしてマークするのをリセットできます。VS Code が埋め込み言語を適切に扱うことを確実にするために、埋め込み言語を常に `meta.embedded.*` スコープでラップすることをお勧めします。
文法に `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 トークンのスコープリストを表示し、最も具体的なスコープが一番上に表示されます。また、スコープに一致する最も具体的なテーマルールも表示します。これは、トークンの現在のスタイルを決定するテーマルールのみを表示し、オーバーライドされたルールは表示しません。セマンティックトークンが存在する場合、テーマルールはセマンティックトークンに一致するルールと異なる場合にのみ表示されます。