VSCodeシンタックスハイライト拡張を最速で作る

1. はじめに ― 自作シンタックスハイライトの意義

みなさまは、独自ドメイン言語(DSL)を VSCode で編集するとき、キーワードもコメントも一様に灰色で表示されている場面に遭遇したことはありませんか。私が業務で扱う TPG(Test Program Generator)という産業機器向けスクリプト言語も、そのままでは読みづらく、ちょっとした入力ミスを見落としやすい状況でした。

本記事では「まずは最低限の色分けを実現する」という現実的な目標を掲げ、VSCode 拡張としてシンタックスハイライトを実装する手順を順を追ってご案内いたします。専門用語はできる限りかみ砕き、手順ごとに背景を説明しますので、拡張開発が初めての方でも安心して読み進めていただけます。


2. 開発環境の準備 ― 必要ツールをそろえる

拡張開発に必要となる主なツールは以下の三つです。

  1. Node.js(LTS 版推奨)
  2. Yeoman & VSCode 拡張ジェネレータgenerator-code
  3. Visual Studio Code 本体

インストールがお済みでない場合は、下記コマンドを参考にセットアップしてください。

# Node.js は公式サイトからインストール済みと仮定
npm install -g yo generator-code   # Yeoman と拡張ジェネレータを全局インストール
code --version                     # VSCode が正しくインストールされているか確認

公式ドキュメントへのリンクを併記いたします。


3. 雛形を作成する ― yo code ウィザードを一歩ずつ進める

まずはターミナルで次のコマンドを実行し、拡張テンプレートを生成します。

yo code

ウィザードでは複数の入力が求められます。代表的な項目と入力例、そして考慮事項を以下の表にまとめました。必要に応じてご自身のプロジェクトに合わせて変更してください。

質問項目入力例ポイント・補足説明
Extension typeNew Languageハイライト用拡張を作成する場合はこれを選択します。
Extension nameTPG Syntax HighlightVSCode の拡張一覧に表示される名称です。
Identifiertpg-syntax-highlightnpm 公開を想定し、短くわかりやすい英小文字+ハイフン推奨。
Description(空欄でも可)後ほど package.json で加筆できます。
Language idtpgVSCode が内部で扱う言語 ID。拡張子とは別概念です。
Language nameTest Programming Generatorステータスバーなどに表示されるフレンドリーな名称。
File extensions.tpg複数ある場合はカンマ区切りで列挙してください。
Scope namesource.tpg後述する scopeName と一致させます。
Initialize git repoYesGit 管理が不要な場合は No でも構いません。

ウィザード完了後、以下のようなディレクトリが生成されます(抜粋)。

tpg-syntax-highlight/
|   package.json
|   README.md
|   language-configuration.json
|
+---syntaxes
|       tpg.tmLanguage.json
|
+---.vscode
        launch.json

ここまでで「骨格」は完成しました。次章からはハイライトの本体である TextMate Grammar を作成していきます。


4. TextMate Grammar を理解する ― tpg.tmLanguage.json の書き方

VSCode は TextMate 形式の文法定義を用いてトークン(キーワードや文字列など)へ色を適用します。ポイントは次の三要素です。

JSON キー役割記述例
scopeName「この文法の全体名」を宣言source.tpg
patterns上から順に評価されるルールの入口{ "include": "#comment" } など
repositoryラベルごとの詳細な正規表現を格納comment, string など

4.1 ラベル設計 ― “何に色を付けるか” を決める

まずは対象言語(TPG)において強調したいトークンを洗い出しましょう。最低限、以下 7 種類があると実務で困りません。

ラベル主な対象推奨スコープ例
comment/* … */ ブロックコメント、行頭 ; コメントcomment.block, comment.line
stringシングルクォート文字列、計測器 %NN エスケープ付き文字列string.quoted.single
number10 / 2 / 8 / 16 進数リテラル、実数リテラルconstant.numeric
keyword制御構文 (IF など)、データ型 (FLOAT など)keyword.control, storage.type
operator!!, .AND., >= など多種多様な演算子keyword.operator
label行頭のジャンプ先 START_LOOP:entity.name.label
identifier予約語を除くすべての識別子variable.other

4.2 例示 ― コメント付き最小構成 JSON

以下は 動作確認用の最小実装 です。実務では正規表現をさらに細分化すると保守しやすくなります。

{
  "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/v1.0.0/tmlanguage.json",
  "name": "Teradyne TPG",
  "scopeName": "source.tpg",
  "patterns": [
    { "include": "#comment" },
    { "include": "#string" },
    { "include": "#number" },
    { "include": "#keyword" },
    { "include": "#operator" },
    { "include": "#label" },
    { "include": "#identifier" }
  ],
  "repository": {
    "comment": {
      "patterns": [
        { "name": "comment.line.semicolon.tpg", "match": ";.*$" },
        {
          "name": "comment.block.tpg",
          "begin": "/\\*",              // `/*` にマッチ
          "end": "\\*/",                // `*/` で閉じる
          "patterns": [ { "include": "#comment" } ] // ネスト対応
        }
      ]
    },
    "string": {
      "name": "string.quoted.single.tpg",
      "begin": "'",
      "end": "'",
      "patterns": [
        {
          "match": "%%|%[0-9A-Fa-f]{2}",
          "name": "constant.character.escape.tpg"
        }
      ]
    },
    "number": {
      "patterns": [
        { "match": "\\b[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?\\b", "name": "constant.numeric.decimal.tpg" },
        { "match": "\\bB'[01]+'", "name": "constant.numeric.binary.tpg" },
        { "match": "\\bO'[0-7]+'", "name": "constant.numeric.octal.tpg" },
        { "match": "\\bX'[0-9A-Fa-f]+'", "name": "constant.numeric.hex.tpg" }
      ]
    },
    "keyword": {
      "patterns": [
        { "match": "\\b(?:IF|THEN|ELSE|END)\\b", "name": "keyword.control.tpg" },
        { "match": "\\b(?:FLOAT|CSTRING|BSTRING|FILE)\\b", "name": "storage.type.tpg" }
      ]
    },
    "operator": {
      "patterns": [
        { "match": "!!", "name": "keyword.operator.concat.tpg" },
        { "match": "\\.(?:AND|OR|NOT)\\.", "name": "keyword.operator.logical.tpg" },
        { "match": "(?:<=|>=|<>|!=|=|<|>|\\+|\\-|\\*|/)", "name": "keyword.operator.tpg" }
      ]
    },
    "label": {
      "match": "^\\s*[A-Za-z_][A-Za-z0-9_]*:\\s*(?=\\S)",
      "name": "entity.name.label.tpg"
    },
    "identifier": {
      "match": "\\b[A-Za-z_][A-Za-z0-9_]{0,23}\\b",
      "name": "variable.other.tpg"
    }
  }
}

4.3 正規表現設計で押さえておくべきポイント

  1. 余白の取り扱いを明確にする — 行頭が揃わないコメントなどは ^\\s* を用いて吸収します。
  2. 終端トークンの誤一致を避ける — 文字列リテラルの閉じクォートは、エスケープ (\\') を許容した上で正しく検出しましょう。
  3. 単語境界だけに頼らない.AND. のようにピリオドに挟まれた演算子は \\. を含めたカスタム境界で拾う必要があります。

補足: 色が反映されないときは VSCode コマンド Developer: Inspect Editor Tokens and Scopes で実際に付与されたスコープ名を確認すると原因究明が容易です。


5. テストとデバッグ ― Extension Development Host を活用する

拡張フォルダを VSCode で開き、F5 キーを押すと Extension Development Host(開発用の別インスタンス)が起動します。そこで .tpg ファイルを作成し、色分けが期待どおりか確認しましょう。tmLanguage.json を保存すると即座に反映されるため、ホットリロード感覚で調整できます。

テスト用コード例:

; Sample TPG
IF SIGNAL_A > 5 THEN
    SET VOLTAGE "CH1" 3.3
ELSE
    LOG "Voltage too low"
END

よくあるつまずきと対処法(3 例)

  • scopeName の不一致package.jsontmLanguage.jsonscopeName が揃っているか確認してください。
  • 正規表現エラー — 無効な正規表現があると該当パターンが無視されます。特にバックスラッシュの JSON エスケープ漏れにご注意を。
  • 拡張子の登録漏れ.tpgcontributes.languages.extensions に含まれていない場合、VSCode が言語を認識できません。

6. 仕上げ ― package.jsonREADME.md を整える

6.1 package.json の必須フィールド

"contributes": {
  "languages": [
    { "id": "tpg", "aliases": ["TPG"], "extensions": [".tpg"] }
  ],
  "grammars": [
    {
      "language": "tpg",
      "scopeName": "source.tpg",
      "path": "./syntaxes/tpg.tmLanguage.json"
    }
  ]
}
  • languages — 言語 ID・表示名・対応拡張子を宣言します。
  • grammars — 文法ファイルへのパスと scopeName を紐付けます。

その他のフィールド(バージョンや出版社名など)はテンプレートのままでも動作しますが、Marketplace に公開する際は適切に記述することをお勧めいたします。

6.2 README.md に最低限含めたい 3 項目

  1. 導入手順 — VSIX ファイルのインストールコマンドや Marketplace URL。
  2. 言語仕様ドキュメントへの導線 — 公式資料や社内 Wiki へのリンク。
  3. ライセンス — MIT ライセンスなら一行で明記できます。

7. まとめ ― まずは“一色分類”から始めよう

自作シンタックスハイライトの核心は「正規表現を書いて即確認する」の繰り返しです。今回ご紹介した最小実装でも、無彩色だった DSL が一気に読みやすくなることを体感いただけるでしょう。

将来的には以下のような機能拡張も視野に入ります。

  • シンボル一覧やジャンプ機能を提供する Document Symbol Provider
  • スニペットやコード補完を追加する Completion Provider
  • Marketplace への公開と自動更新対応

まずはご自身が日常的に触れている DSL を対象に、10 行の正規表現から始めてみてはいかがでしょうか。きっと開発効率が一段上がるはずです。


参考資料

  • Visual Studio Code Extension API(公式)
  • TextMate Grammar Documentation
  • generator-code GitHub リポジトリ
  • Regular Expressions 101(オンライン正規表現テスター)

コメントする

CAPTCHA