Sublime Text2 言語パッケージ作成:構文定義

  • 56
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

補完定義編 の続き(のつもり)です。この記事の内容は、Sublime Text ドキュメントの Syntax Definitionsを元ネタとしています。

事前準備

AAAPackageDev をインストールします。

構文定義ファイル自体は Plist という XML もどきの形式ですが、長ったらしくて書くのが面倒なため、JSON で書きます。AAAPackageDev は JSON で書いた構文定義を Plist に変換してくれるツールです。

JSON-tmLanguage の作成

メニューから Tools > Packages > Package Development> New Syntax Definition とアクセスして、ファイルを作成します。エディタにこのように表示されているはずです。

***.JSON-tmLanguage
{ "name": "Syntax Name",
  "scopeName": "source.syntax_name",
  "fileTypes": [""],
  "patterns": [
  ],
  "uuid": "9d286105-1dd9-464e-b197-3c24689d804e"
}

name エディタの右下に表示される名前です。このファイルを開いているときは、右下には Sublime Text Syntax Definition と表示されているはずです。この部分にここで指定した名前が入ります。

scopeName スコープの名称です。ソースファイルの場合は source. 、マークアップの場合は text. の後に、言語の小文字の短縮名をつけた文字列とします。補完定義編で指定したものと同じものを使います。

uuid 構文を識別する一意名です。このファイルを生成するたびに変わります。手で書き換えることはありません。

fileTypes この構文を適用するファイルの拡張子を指定します。補完定義編で指定したものと同じものを使います。

patterns 後述します。

tmLanguage への変換

  1. Tools > Build System で Json to tmLanguage を選択します。(メニューに表示されていない場合は、Sublime を再起動します)
  2. F7 キーを押します。
  3. .JSON-tmLanguage ファイルと同じフォルダに.tmLanguage が作成されます。つまり、補完定義編で作成した .tmLanguage が上書きされます。

ここで fileTypes で指定した拡張子で何かファイルを作って、補完定義編で定義したキーワードをタイプしてみます。無事に補完されていたら大丈夫です。

構文定義の意味

構文定義により、ファイルの中身がスコープに区切られます。スコープは次の用途で使われます。

  • 構文の色づけ
  • マクロやスニペットで、対象範囲を特定する

現在は、ファイル全体が scopeName で指定されたスコープです。対象のファイルを開き、エディタの適当な場所をクリックして、command + alt+ p (または ctrl+ shift+ p) を押してみると、source.構文名 と表示されるはずです。今から、構文パターンによって違う名前のスコープができるように pattern に追加していきます。

超単純なマッチパターン

基本形はこれ。

***.JSON-tmLanguage
  "patterns": [
    { "match": "\\$\\d+",
      "name": "keyword.source.syntax_name",
      "comment": "$1, $2... みたいな感じ"
    }
  ],

この例では、"\$\d+" パターンにあてはまる部分の文字列が keyword.source.syntax_name スコープとなります。

match 構文パターンの正規表現。JSON 向けにエスケープして書きます。

name この構文パターンを識別する名前。

comment 構文パターンの説明。

マッチンググループを使ったマッチパターン

もうちょっと複雑にして、パターンにマッチンググループを入れてみます。カッコでくくると、マッチンググループとなります。

***.JSON-tmLanguage
  "patterns": [
    { "match": "\\$([A-Za-z][A-Za-z0-9_]+)",
      "name": "keyword.syntax_name",
      "captures": {
      "1": { "name": "constant.numeric.syntax_name" }
       },
      "comment": "$PARAM1, $TM_SELECTION... みたいな感じ"
    }
  ],

この例では、例えば $PARAM1 の場合、$PARAM1 全体が keyword.syntax_name スコープとなり、さらに PARAM1 の部分に constant.numeric.syntax_name スコープが追加されます。

captures マッチンググループごとのスコープ定義。巷の言語の正規表現と同じく、1から始まります。match パターンにマッチンググループがあっても、ここでスコープを定義してなければ、name で定義したスコープだけが適用されます。

開始要素・終了要素があるマッチパターン

基本的に

構文
{ "name": "",
  "begin": "",
  "beginCaptures": {
    "0": { "name": "" }
  },
  "end": "",
  "endCaptures": {
    "0": { "name": "" }
  },
  "patterns": [
     {  "name": "",
        "match": ""
                  }
  ],
  "contentName": ""
}

name は同じです。

beginend には構文の開始終了パターン(開始終了タグのようなもの)を定義します。

マッチンググループを作った場合、それぞれ、beginCapturesendCaptures でグループごとのスコープを定義できます。

patterns は開始終了パターン以外の部分のマッチパターンです。後述。

contentName には patterns の部分に適用されるスコープの名前を定義します。(元ネタは開始終了パターンを含むと書いてありますが、間違いでは?)

例えばこのパターンで ${12:${34:TEST}} を解析すると、次のようになります。

***.JSON-tmLanguage
   "patterns": [
       { "name": "variable.complex.syntax_name",
          "begin": "(\\$)(\\{)([0-9]+):",
          "beginCaptures": {
              "1": { "name": "keyword.syntax_name" },
              "3": { "name": "constant.numeric.syntax_name" }
           },
           "patterns": [
              { "include": "$self" },
              { "name": "string.syntax_name",
                "match": "."
              }
           ],
           "end": "\\}",
           "comment": "${12:${34:TEST}} … みたいな感じ"
        },
  ],
  1. 全体のスコープとして variable.complex.syntax_name が割り当てられます。
  2. ${12: と、${34:TEST} と、} に3分割されます。
  3. beginCaptures により、$ の部分には keyword.syntax_name スコープが割り当てられ、12 の部分には constant.numeric.syntax_name が割り当てられます。
  4. ${34:TEST} の解析が始まりますが、その前に "include": "$self" により、このパターンに再帰的に当てはめられます。つまり、2. の手順で3分割され、3. の手順で $ と 34 にそれぞれスコープが割り当てられます。
  5. 残った文字列 TEST に、`"include": "$self" でまたマッチング処理が行われますが、今回はマッチしません。このため、次の match のマッチング処理が行われ、(必ずヒットするので)この文字列に name で定義された string.syntax_name が割り当てられます。

マッチの順番

patterns に書かれている中で、上から順番にマッチされていくため、既にマッチされた部分の文字列は除外されていきます。このため、次のような書き方で、エスケープしていない記号を検出できます。

***.JSON-tmLanguage
   "patterns": [
        { "name": "constant.character.escape.syntax_name",
          "match": "\\\\(\\$|\\>|\\<)"
        },

        { "name": "invalid.ssraw",
          "match": "(\\$|\\>|\\<)"
        }
  ],

これまでの説明で一通りの構文パターンが書けます。TextMate のマニュアルを見ると、更に省力化して書く方法も載ってはいますが。

で、どうやって色がつくの?

この答えとなるようなものは元ネタに載っていないため、TextMate のリファレンスを参考にします。

スコープの名前の先頭が次の文字で始まっていれば、Sublime の機能によって自動的にそれなりの色がつきます。

  • comment. コメント
  • constant. 定数
  • entity. クラスとか、ファンクションとか、タグとかの、ちょっと大きめの単位
  • invalid. けしからんもの(どす黒い赤のマーカーがつく)
  • keyword. キーワード
  • markup. マークアップ文書のテキスト
  • meta. 関数定義など (色がつくことはほとんどないので、単純に操作範囲を狭めたいときに利用)
  • storage. 型とか修飾子とか
  • string. 文字列
  • support. (言語機能でなく)フレームワークで提供されるもの
  • variable. 変数名

さらに細かく分かれているので、全部の説明はTextMate のリファレンスを参照してください。