シンタックスハイライトガイド

シンタックスハイライトは、Visual Studio Code エディターに表示されるソースコードの色とスタイルを決定します。これは、JavaScript の iffor のようなキーワードを、文字列、コメント、変数名とは異なる色で表示する役割を担っています。

シンタックスハイライトには 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 エディターのために発明された TextMate グラマーは、オープンソースコミュニティによって作成および保守されている多数の言語バンドルのおかげで、他の多くのエディターや IDE にも採用されています。

TextMate グラマーは、Oniguruma 正規表現に依存しており、通常は plist または JSON として記述されます。こちらに TextMate グラマーの良い入門書があります。また、既存の TextMate グラマーを見て、それらがどのように機能するかを詳しく学ぶこともできます。

TextMate トークンとスコープ

トークンは、同じプログラム要素の一部である 1 つ以上の文字です。トークンの例としては、+* などの演算子、myVar などの変数名、"my string" などの文字列などがあります。

各トークンは、トークンのコンテキストを定義するスコープに関連付けられています。スコープは、現在のトークンのコンテキストを指定するドット区切りの識別子のリストです。たとえば、JavaScript の + 演算子のスコープは keyword.operator.arithmetic.js です。

テーマは、シンタックスハイライトを提供するために、スコープを色とスタイルにマッピングします。TextMate は、多くのテーマがターゲットとする 一般的なスコープのリストを提供しています。グラマーをできるだけ広くサポートできるようにするには、新しいスコープを定義するのではなく、既存のスコープに基づいて構築するようにしてください。

スコープはネストされているため、各トークンは親スコープのリストにも関連付けられています。以下の例では、スコープインスペクターを使用して、単純な JavaScript 関数内の + 演算子のスコープ階層を示しています。最も具体的なスコープが一番上にリストされ、より一般的な親スコープが下にリストされています。

syntax highlighting scopes

親スコープ情報もテーマに使用されます。テーマがスコープをターゲットにすると、テーマが個々のスコープに対してより具体的な色付けを提供しない限り、その親スコープを持つすべてのトークンが色付けされます。

基本的なグラマーの提供

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 グラマーは、文字 abc をキーワードとしてマークし、括弧のネストを式としてマークします。

{
  "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-of-documentend ルールの前に見つかったためです。

埋め込み言語

グラマーに、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 オプションを選択します。

Selecting the 'new language' template in 'yo code'

Yeoman は、新しい拡張機能を足場にかけるためのいくつかの基本的な質問を順に尋ねます。新しいグラマーを作成する際の重要な質問は次のとおりです。

  • Language id - 言語の一意の識別子。
  • Language name - 言語の人間が読める名前。
  • Scope names - グラマーのルート TextMate スコープ名。

Filling in the 'new language' questions

ジェネレーターは、新しい言語と新しい言語のグラマーの両方を定義することを前提としています。既存の言語のグラマーを作成する場合は、これらにターゲット言語の情報のみを入力し、生成された package.jsonlanguages 貢献ポイントを必ず削除してください。

すべての質問に答えると、Yeoman は次の構造で新しい拡張機能を作成します。

A new language extension

覚えておいてください。VS Code がすでに認識している言語にグラマーを提供する場合は、生成された package.jsonlanguages 貢献ポイントを必ず削除してください。

既存の TextMate グラマーの変換

yo code は、既存の TextMate グラマーを VS Code 拡張機能に変換するのにも役立ちます。もう一度、yo code を実行して Language extension を選択することから始めます。既存のグラマーファイルを尋ねられたら、.tmLanguage または .json TextMate グラマーファイルへのフルパスを指定します。

Converting an existing TextMate grammar

YAML を使用してグラマーを記述する

グラマーが複雑になるにつれて、json として理解および保守することが難しくなる場合があります。複雑な正規表現を記述したり、グラマーの側面を説明するためにコメントを追加する必要がある場合は、代わりに yaml を使用してグラマーを定義することを検討してください。

Yaml グラマーは、json ベースのグラマーとまったく同じ構造を持ちますが、yaml のより簡潔な構文と、複数行文字列やコメントなどの機能を使用できます。

A yaml grammar using multiline strings and comments

VS Code は json グラマーのみをロードできるため、yaml ベースのグラマーは json に変換する必要があります。js-yaml package とコマンドラインツールを使用すると、これが簡単になります。

# 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 ファイルにインジェクショングラマーを適用するには、injectTosource.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"
}

scope inspector

スコープインスペクターには、次の情報が表示されます。

  1. 現在のトークン。
  2. トークンに関するメタデータと、計算された外観に関する情報。埋め込み言語を使用している場合、ここでの重要なエントリは languagetoken type です。
  3. セマンティックトークンセクションは、現在の言語でセマンティックトークンプロバイダーが利用可能であり、現在のテーマがセマンティックハイライトをサポートしている場合に表示されます。現在のセマンティックトークンタイプと修飾子、およびセマンティックトークンタイプと修飾子に一致するテーマルールが表示されます。
  4. TextMate セクションには、現在の TextMate トークンのスコープリストが、最も具体的なスコープが一番上に表示されます。また、スコープに一致する最も具体的なテーマルールも表示されます。これは、トークンの現在のスタイルを担当するテーマルールのみを表示し、オーバーライドされたルールは表示しません。セマンティックトークンが存在する場合、テーマルールはセマンティックトークンに一致するルールと異なる場合にのみ表示されます。