プログラミング言語のソースに、他の言語のスニペットが含まれていることがあります。HTML + CSS + JavaScript がその一例です。このような場合、異なる言語で記述されたテキストセグメントには、それぞれ異なるパーサーを割り当てる必要があります。従来、これはナローイングを用いて実現されてきました。Tree-sitter もナローイングに対応していますが(ナローイングを参照)、推奨される方法は、代わりにパーサーが動作するバッファテキストの領域(すなわちレンジ)を指定することです。本節では、パーサーのレンジを設定および取得するための関数について説明します。
一般的に、複数の言語が混在する場合、「プライマリ」または「ホスト」言語が存在します。この言語用のパーサー――プライマリパーサー――は、バッファ全体を解析します。他の言語用のパーサーは「埋め込み」または「ゲスト」パーサーであり、バッファの一部に対してのみ動作します。プライマリパーサーの構文木は、通常、埋め込みパーサーが動作する範囲を決定するために用いられます。
主要なモードは、treesit-major-mode-setupを呼び出す前にtreesit-primary-parserをプライマリパーサーに設定すべきであり、これによりEmacsがfont-lockやその他の機能向けにプライマリパーサーを正しく設定できるようになります。
Lispプログラムでは、バッファ内でパーサーを使用する前に、各パーサーの範囲が正しいことを確認するために treesit-update-ranges を呼び出し、ある位置にあるテキストの言語を特定するために treesit-language-at
を呼び出す必要があります。これら2つの関数は単独で動作するものではなく、実際の処理を担うメジャーモードが treesit-range-settings および(必要に応じて)treesit-language-at-point-function
を設定する必要があります。これらの関数と変数については、本節の後半でより詳しく解説します。
要するに、多言語対応のメジャーモードでは、treesit-major-mode-setupを呼び出す前にtreesit-primary-parser、treesit-range-settings、および必要に応じてtreesit-language-at-point-functionを設定すべきである。
この関数は、parser が ranges
に対して動作するように設定します。parser は、指定された範囲のテキストのみを読みます。ranges の各範囲は、(beg . end)
という形式のペアです。
ranges の範囲は順に並び、かつ重複してはなりません。つまり、擬似コードでは次のようになります:
(cl-loop for idx from 1 to (1- (length ranges))
for prev = (nth (1- idx) ranges)
for next = (nth idx ranges)
should (<= (car prev) (cdr prev)
(car next) (cdr next)))
rangesがこの制約に違反した場合、あるいはその他の問題が発生した場合、この関数はtreesit-range-invalidエラーを通知します。シグナルデータには、具体的なエラーメッセージと、設定しようとしている範囲が含まれています。
この関数は、範囲の無効化にも使用できます。ranges が nil の場合、パーサーはバッファ全体を解析するように設定されます。
例:
(treesit-parser-set-included-ranges parser '((1 . 9) (16 . 24) (24 . 25)))
この関数は、parser に設定された範囲を返します。戻り値は treesit-parser-included-ranges の引数 ranges と同じであり、(beg . end)
形式のコンスセルのリストとなります。parser に範囲が設定されていない場合、戻り値は nil となります。
(treesit-parser-included-ranges parser)
⇒ ((1 . 9) (16 . 24) (24 . 25))
この関数は source を query
と照合し、キャプチャされたノードの範囲を返します。戻り値は (beg . end)
形式のコンスセルのリストであり、ここで beg と end
はテキスト領域の開始位置と終了位置を指定します。
便宜上、source は言語シンボル、パーサー、またはノードのいずれかになり得ます。言語シンボルである場合、この関数はその言語を使用する最初のパーサーのルートノードでマッチします。パーサーである場合、この関数はそのパーサーのルートノードでマッチします。ノードである場合、この関数はそのノードでマッチします。
引数 query は、ノードをキャプチャするために使用されるクエリです(パターンマッチング Tree-sitter ノード
を参照)。キャプチャ名は問いません。引数 beg と end
がともに nil 以外である場合、この関数がクエリを行う範囲が制限されます。
他のクエリ関数と同様に、この関数は query の形式が不正な場合に treesit-query-error エラーを発生させます。
複数の言語が混在するプログラムソースに対応するには、一般的なLispプログラムで以下の2つの関数を呼び出せば十分でしょう。
この関数は、バッファ内のパーサーの範囲を更新します。treesit-range-settings
に基づき、beg から end
の間でパーサーの範囲が正しく設定されるようにします。省略した場合、beg はバッファの先頭に、end はバッファの末尾にそれぞれデフォルトで設定されます。
例えば、フォント化関数では、領域内のノードをクエリする前にこの関数を使用します。
この関数は、バッファ位置 pos にあるテキストの言語を返します。内部的には treesit-language-at-point-function
を呼び出し、その戻り値を返します。treesit-language-at-point-function が nil である場合、この関数は treesit-parsers-at によって返されたパーサーのうち、埋め込みレベル(parser-embed-level
を参照)が最も深いパーサーの言語を返します。そのバッファ位置にパーサーが存在しない場合は、nil
を返します。
通常、混在可能な言語群には、ホスト言語と1つ以上の埋め込み言語が存在します。Lispプログラムは通常、まずホスト言語のパーサーで文書全体を解析して一部の情報を取得し、その情報を用いて埋め込み言語の範囲を設定した上で、埋め込み言語を解析します。
HTML、CSS、およびJavaScriptを含むバッファを例に挙げます。Lispプログラムはまず、HTMLパーサーでバッファ全体を解析し、次にパーサーにstyle_elementおよびscript_elementノードを問い合わせます。これらはそれぞれCSSとJavaScriptのテキストに対応しています。その後、CSSおよびJavaScriptパーサーの範囲を、対応するノードが跨ぐ範囲に設定します。
次のようなシンプルなHTMLドキュメントがあるとします:
<html>
<script>1 + 2</script>
<style>body { color: "blue"; }</style>
</html>
Lispプログラムは、まずHTMLパーサーで解析し、次にCSSおよびJavaScriptパーサー用の範囲を設定します:
;; Create parsers. (setq html (treesit-parser-create 'html)) (setq css (treesit-parser-create 'css)) (setq js (treesit-parser-create 'javascript))
;; Set CSS ranges.
(setq css-range
(treesit-query-range
'html
'((style_element (raw_text) @capture))))
(treesit-parser-set-included-ranges css css-range)
;; Set JavaScript ranges.
(setq js-range
(treesit-query-range
'html
'((script_element (raw_text) @capture))))
(treesit-parser-set-included-ranges js js-range)
Emacsはtreesit-update-rangesでこの処理を自動化します。多言語メジャーモードはtreesit-range-settingsを設定し、treesit-update-rangesがこの処理を自動的に実行できるようにすべきです。メジャーモードはヘルパー関数treesit-range-rulesを使用して、treesit-range-settingsに代入可能な値を生成する必要があります。以下の例の設定は、上述の操作に直接対応しています。
(setq treesit-range-settings
(treesit-range-rules
:embed 'javascript
:host 'html
'((script_element (raw_text) @capture))
:embed 'css
:host 'html
'((style_element (raw_text) @capture))))
;; Major modes with multiple languages can optionally set
;; `treesit-language-at-point-function' (which see).
(setq treesit-language-at-point-function
(lambda (pos)
(let* ((node (treesit-node-at pos 'html))
(parent (treesit-node-parent node)))
(cond
((and node parent
(equal (treesit-node-type node) "raw_text")
(equal (treesit-node-type parent) "script_element"))
'javascript)
((and node parent
(equal (treesit-node-type node) "raw_text")
(equal (treesit-node-type parent) "style_element"))
'css)
(t 'html)))))
この関数は treesit-range-settings
を設定するために使用されます。クエリのコンパイルやその他の後処理を行い、treesit-range-settings が取り得る値を出力します。
一連の query-spec で構成され、各 query-spec は、0個以上の keyword/value ペアに先行する query となります。各 query は、文字列、s-expression、コンパイル済み形式のいずれかにおける Tree-sitter クエリ、または関数です。
treesit-update-ranges は query を用いて、埋め込み言語用パーサーの範囲をどのように設定するかを決定します。ホスト言語パーサーの query
にクエリを投げ、キャプチャされたノードが跨る範囲を算出し、これらの範囲を埋め込み言語用パーサーに適用します。
query が Tree-sitter クエリである場合、その前には 2 組の keyword/value
ペアが配置される必要があります。ここで、:embed キーワードで埋め込み言語を、:host
キーワードでホスト言語を指定します。キャプチャされたノードによって、埋め込みパーサーの範囲が決定されます。キャプチャ名は任意ですが、アンダースコアで始まる名前は無視され、@language というキャプチャ名は予約されています。
埋め込み言語が動的な場合、:embed
では静的言語ではなく関数を指定できます。この関数は、@language
のキャプチャ名で捕捉されたノードを引数として受け取ります。@language
のキャプチャと範囲キャプチャが同じマッチグループ内にあるように注意する必要があります。関数は、言語シンボルを返すか、有効な言語が見つからない場合は
nil を返す必要があります。
クエリに数値のペアを値とする :offset
キーワードが指定された場合、キャプチャされた各範囲はその数値分だけオフセットされます。例えば、(1
. -1) というオフセットを指定すると、(2 . 8) のキャプチャ範囲が
(3 . 7)
に更新されます。これにより、周囲のデリミタなどが埋め込みパーサーのカバー範囲に含まれないようにすることができます。
デフォルトでは、パーサーは範囲を個別の独立したセグメントとして扱うのではなく、連続的なものとして認識します。したがって、埋め込まれた範囲が意味的に独立したセグメントである場合は、以下に説明するローカルパーサーによって処理されるべきです。
クエリに値 t を持つキーワード :local
が指定されている場合、このクエリで設定される範囲には専用のローカルパーサーが割り当てられます。そうでない場合、その範囲は同じ言語の他の範囲とパーサーを共有します。
範囲に設定されたローカルパーサーは、treesit-local-parsers-at および
treesit-local-parsers-on で取得できます。
Emacsでは、ローカル埋め込みパーサーと非ローカル埋め込みパーサーを全く異なる扱いで処理します:
トップレベルパーサーの埋め込みレベルは0または省略され、埋め込みパーサーの第1レベルは埋め込みレベル1となり、以降も同様です。ローカルパーサーの場合、埋め込みレベルごとに新しいパーサーが作成されます。非ローカルパーサーの場合、Emacsは非ローカルパーサーを自由に作成できないため、適切な埋め込みレベルで利用可能なパーサーを探そうとします。もし見つからなければ、Emacsはそのまま処理を断念します。
クエリに値が関数である :range-fn キーワードが指定されている場合、Emacs
はその関数を用いて、埋め込みパーサーで使用する範囲を算出します。この関数は、キャプチャされたノードと :offset キーワードで指定されたオフセットを引数として受け取り、各範囲が開始位置と終了位置の cons
である範囲のリストを返します。ローカルパーサーの場合、各 query
が1つのローカルパーサーを生成し、:range-fn
で設定された範囲がそのパーサーに適用されます。非ローカルパーサーの場合、その埋め込みレベルにあるすべての範囲(:range-fn で生成された範囲を含む)が結合され、同一のパーサーに適用されます。
query が関数である場合、keyword と value のペアは不要です。これは、2つの引数 start と end を受け取り、現在のバッファ内のパーサーの範囲を start と end の間の領域に設定する関数であるべきです。この関数が、start と end の間の領域を包含するより広い領域に範囲を設定しても問題ありません。
この変数は、treesit-update-ranges
がバッファ内のパーサーの範囲を更新する際に役立ちます。これは setting のリストであり、setting の正確な形式は内部的に処理されます。この変数が取り得る値を生成するには、treesit-range-rules を使用してください。
この変数の値は、バッファ位置である単一の引数 pos を受け取り、pos にあるバッファテキストの言語を返す関数である必要があります。この変数は treesit-language-at で使用されます。
この関数は、現在のバッファ内の pos にあるすべてのパーサーを返します。pos のデフォルト値は point です。返されるパーサーは、埋め込みレベルの降順でソートされます。
language が nil
でない場合、その言語のパーサーのみを返す。
with-host が nil でない場合、parser を生成したホストパーサーを host-parser
とする (parser . host-parser) のリストを返す。
only が nil
でない場合、プライマリパーサーを含むすべてのパーサーを返す。
引数 only には、戻り値に含めるパーサーを指定するシンボルのリストを指定できます。
only にシンボル local
が含まれている場合は、ローカルパーサーを含めてください。ローカルパーサーとは、nil treesit-parser-local-p
プロパティを持たないオーバーレイでマークされた限定的な領域のみを解析するパーサーのことです。
only に記号 global
が含まれる場合、プライマリパーサーを除く非ローカルパーサーを含める。
only に記号 ‘primary’ が含まれている場合は、プライマリパーサーを含めてください。
この関数は、現在のバッファ内の pos にあるすべてのローカルパーサーを返します。pos のデフォルト値は point です。
ローカルパーサーとは、non-nil treesit-parser-local-p
プロパティを持つオーバーレイで指定された限定的な領域のみを解析するパーサーのことです。language が
non-nil である場合は、その言語のパーサーのみを返します。
この関数は treesit-local-parsers-at と同じですが、point
ではなく、beg から end
の範囲にあるローカルパーサーを返します。
beg および end は、デフォルトでバッファのアクセス可能な全領域を対象とします。