🧊

Nvimと`nvim-treesitter`と`tree-sitter-markdown`の関係

あれこれ調べたり聞いたりしたまとめ。

あらまし

  • Markdownを書くことが多いので、そのシンタックスハイライトにはちょっとだけこだわりがあった
  • 具体的には、# タイトルと書くとき、#タイトルを別の色にしたい
  • なので、それができるカラースキームを探して使ってた

というのが、だいたい去年の秋くらいまでの話。

ところが、ある日を境にそれができなくなった。

なんで?とは思いつつ、その原因を探るヒマがないまま月日は流れ・・・、今回その理由をやっと解明できた!という話。

TL;DR

Markdown missing header mark highlights · Issue #6260 · nvim-treesitter/nvim-treesitter https://github.com/nvim-treesitter/nvim-treesitter/issues/6260

このIssueがずばりそれ。

  • @markup.heading.1.marker.markdownというハイライトグループが
  • ある日のコミットからなくなってしまった
  • #部とタイトル部がまとめて@markup.heading.1.markdownになってしまった

というのが原因で、その理由はメンテナンス性のためだそうな・・・。

詳しい事情は知らんけど、まあなんかいろいろあるんでしょう。

それぞれの関係

さて本題。

このあたりの仕組みを理解するには、登場人物とその役割を把握する必要がある。

  • tree-sitter(ライブラリ)
  • tree-sitter-markdown(ライブラリ)
  • Nvim(のTreesitterインテグレーション)
  • nvim-treesitter(プラグイン)

まず、tree-sitterそれ自体は、Cで書かれたソースコードをインクリメンタルに解析するためのライブラリ。

tree-sitter/tree-sitter: An incremental parsing system for programming tools https://github.com/tree-sitter/tree-sitter

で、その構文解析機を拡張する言語ごとのパーサーがいろいろあり、tree-sitter-markdownはそのMarkdown向けのもの。

tree-sitter-grammars/tree-sitter-markdown: Markdown grammar for tree-sitter https://github.com/tree-sitter-grammars/tree-sitter-markdown/

ここまでの話には、まだNvimは出てこない。

Treesitter自体は汎用的なもので、シンタックスハイライトにも使えるけど、用途はそれだけに限らない。 たとえばast-grepなんかはTreesitterを使ってASTを生成してるし、Nvimでもインデント検知や折りたたみなどなどの用途もある。

で、Nvimには(Experimentalではあるが)Treesitterインテグレーションが入ってる。

Treesitter - Neovim docs https://neovim.io/doc/user/treesitter.html

ただデフォルトでは、いくつかの言語向けのパーサーしか同梱されていないらしい。

そこで、nvim-treesitterプラグインなり手動でやるなり、足りないものは自分でパーサーを追加してねという建付けとのこと。

パーサーとクエリスキーマとハイライトグループ

tree-sittertree-sitter-markdownの世界で得られるのは、あくまでTreesitterでソースコードをパースして得られた構文木だけ。

(document ; [0, 0] - [72, 0]
  (section ; [6, 0] - [20, 0]
    (atx_heading ; [6, 0] - [7, 0]
      (atx_h2_marker) ; [6, 0] - [6, 2]
      heading_content: (inline)) ; [6, 3] - [6, 15]
    (list ; [8, 0] - [12, 0]
      (list_item ; [8, 0] - [9, 0]
        (list_marker_minus) ; [8, 0] - [8, 2]
        (paragraph ; [8, 2] - [9, 0]
          (inline))) ; [8, 2] - [8, 130]
      (list_item ; [9, 0] - [10, 0]
        (list_marker_minus) ; [9, 0] - [9, 2]
        (paragraph ; [9, 2] - [10, 0]
          (inline))) ; [9, 2] - [9, 98]
      (list_item ; [10, 0] - [12, 0]
        (list_marker_minus) ; [10, 0] - [10, 2]
        (paragraph ; [10, 2] - [11, 0]
          (inline) ; [10, 2] - [10, 77]
          (block_continuation)))) ; [11, 0] - [11, 0]
    (paragraph ; [12, 0] - [13, 0]
      (inline)) ; [12, 0] - [12, 66]
    (paragraph ; [14, 0] - [15, 0]
      (inline)) ; [14, 0] - [14, 66]

こういうやつ。:InspectTreeってやると見えるやつ。

で、これをシンタックスハイライトに使うためには、この木のノードの名称、つまりはキャプチャ名を、Nvimのハイライトグループにマッピングする必要がある。

それをやってるのは、highlights.scmという名前のクエリファイルの呼ばれるもの。

[
  (atx_h1_marker)
  (atx_h2_marker)
  (atx_h3_marker)
  (atx_h4_marker)
  (atx_h5_marker)
  (atx_h6_marker)
  (setext_h1_underline)
  (setext_h2_underline)
] @punctuation.special

たとえばこれは、atx_h1_markerなどのキャプチャされたノードを、@punctuation.specialというハイライトグループに指定するという定義。 :Insepctすると、カーソルのある場所の指定がわかる。

ちなみに以前はTSプレフィックスのついたハイライトグループが使われてたけど、最近は@キャプチャ名をそのまま使えるように変更されたらしい。

ともあれ、これでvim.api.nvim_set_hl(0, "@punctuation.special", { link = "Delimiter" })のように書けばハイライトが適応できるし、各カラースキームが頑張って定義してくれてる。

実際にインストールされるファイルたち

クエリファイル自体は、nvim-treesitterに同梱されてる。

リポジトリでいうと、/queries/{lang}/highlights.scmにあるのがそれ。

なので、なんらかのプラグインマネージャーを使ってても、nvim-treesitterのファイルが配置されるところを見れば、そのクエリファイルを見つけることができる。 人によって違うかもしらんけど、手元のlazy.nvim環境の場合は、~/.local/share/nvim/lazy/nvim-treesitterにあった。

紛らわしいのは、

というところ。

そしてこれらは同期されてるわけではない。ざっくり調べた限り、各拡張側のクエリファイルはしばらくアップデートされてない感じが多かった。

個人的にもうひとつ紛らわしく感じたのは、nvim-treesitterにリポジトリに、lockfile.jsonという各拡張ごとにコミットハッシュが列挙されたファイルがあること。

https://github.com/nvim-treesitter/nvim-treesitter/blob/master/lockfile.json

これは、パーサーのバージョンを追跡するためだけに使われるもので、:TSUpdateした時に、このバージョンの変更に従ってパーサーをリビルドするためのもの。

そのコミットハッシュにおける、各拡張側のクエリファイルが使われるわけではない。

というわけで

それぞれの関係性がわかったところで、冒頭の#だけ色を変えたいケースにどう対応すればいいかというと、現状では自分でクエリファイルを用意するしかなさそう。

2022年の nvim-treesitter の変更・新機能を振り返る https://zenn.dev/vim_jp/articles/2022-12-25-vim-nvim-treesitter-2022-changes#%E4%BB%8A%E5%BE%8C%E3%81%A9%E3%81%86%E3%81%99%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84%E3%81%8B-1

そもそもなんでクエリの定義内容を変えたんよ?って思うけど、クエリの定義を他のエディタと共有できるようにしたいなど、水面下の動きもあるらしい。

Roadmap: Nvim-treesitter 1.0 · Issue #4767 · nvim-treesitter/nvim-treesitter https://github.com/nvim-treesitter/nvim-treesitter/issues/4767

奥が深い。