nvim-treesitterで自作パーサーのハイライトが効かずにハマった話 (Unreal C++編)
Unreal EngineのC++方言(UCLASS
やUPROPERTY
などのマクロ)に対応するため、tree-sitter-cpp
をフォークしてtree-sitter-unreal-cppというパーサーを自作しました。
構文木の定義自体は、元のC++パーサーにUCLASS
などのトップレベル宣言を追加することで比較的すんなり進みました。しかし、これを Neovim (nvim-treesitter
) で使おうとしたところ、シンタックスハイライトが全く効かずにどハマりしてしまいました…
今回は、その解決までの道のりを備忘録としてまとめます。同じように「自作パーサーの構文解析はできてるのにハイライトだけが効かない!」と憤慨している方の助けになれば幸いです。
直面した問題:構文は解析できてるのに、色がつかない!
まず、tree-sitter generate
やtree-sitter test
が正常に通ることを確認した上で、lazy.nvim
を使って以下のように設定しました。nvim-treesitter
が標準で使っているcpp
パーサーを、ローカルで開発している自作パーサーで上書きする作戦です。
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
dependencies = {
"nvim-treesitter/nvim-treesitter-textobjects",
},
main = 'nvim-treesitter.configs',
opts = {
ensure_installed = { "c", "c_sharp", "lua", "vim", "vimdoc" },
-- ... other settings
},
config = function(_,opts)
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
-- cppパーサーの設定をunreal-cppで上書き
parser_config.cpp = {
install_info = {
url = "https://github.com/taku25/tree-sitter-unreal-cpp",
files = {
"src/parser.c",
"src/scanner.c",
},
branch = "master",
},
filetype = "cpp",
}
require("nvim-treesitter.configs").setup(opts)
end,
},
この設定でNeovimを起動し、:TSPlaygroundToggle
コマンドで構文木を確認すると、uclass_macro
などの自作ノードが正しく解析されていることが確認できました。
「よし、これでハイライトも完璧だ!」と思ったのですが、現実は非情でした。エディタ上では全く色がつきません。
さらに、カーソル下の構文情報を確認する:TSCaptureUnderCursor
を実行しても、ハイライトグループが表示されず、ただ困惑するばかりでした。
原因:nvim-treesitterは"君の"ハイライトクエリを読んでくれない
色々調査した結果、衝撃の事実が判明しました。
nvim-treesitter
は、パーサー(parser.c
など)はローカルのものを読んでくれるものの、ハイライトクエリ(highlights.scm
)は自作リポジトリ内のものを参照せず、nvim-treesitter
本体に内蔵されているqueries/{filetype}/highlights.scm
を使い続ける という仕様だったのです。(なぜ…?)
つまり、いくらtree-sitter-unreal-cpp
リポジトリ内のqueries/highlights.scm
を編集しても、Neovimはそれを一切読み込んでいなかった、というわけです。
解決策:after/queries
でハイライト定義を"追加"する
この問題を解決する鍵は、Neovimのafter
ディレクトリの仕組みにありました。Neovimは、設定ファイルを読み込む際に、after
ディレクトリにある同名のファイルを後から追加で読み込んでくれます。
これを利用して、以下の手順でハイライトを適用させることができました。
-
Unreal Engine用のハイライトクエリを別ファイルに分離する
tree-sitter-unreal-cpp
リポジトリ内に、after/queries/cpp/highlights.scm
というパスでファイルを作成します。ここに、UCLASS
などのUnreal Engine専用構文のハイライト定義だけを記述します。 -
tree-sitter-unreal-cpp
自体をプラグインとして読み込ませる
作成したafter
ディレクトリをNeovimに認識させるため、tree-sitter-unreal-cpp
のリポジトリ自体をlazy.nvim
でプラグインとして読み込みます。
この方法により、nvim-treesitter
がまず標準のcpp
用ハイライト(queries/cpp/highlights.scm
)を読み込み、その後に自作のUnreal C++用ハイライト(after/queries/cpp/highlights.scm
)が追加で読み込まれるようになります。
最終的な設定
最終的に、lazy.nvim
の設定は以下のようになりました。nvim-treesitter
の設定に加えて、tree-sitter-unreal-cpp
を独立したプラグインとして追加しているのがポイントです。
-- ★★ 自作パーサーのリポジトリをプラグインとして追加 ★★
-- これでafter/queries/cpp/highlights.scmが読み込まれるようになる
{
"taku25/tree-sitter-unreal-cpp",
},
{
-- nvim-treesitter本体の設定
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
dependencies = {
"nvim-treesitter/nvim-treesitter-textobjects",
},
main = 'nvim-treesitter.configs',
opts = {
-- ...
},
config = function(_,opts)
-- ★★ パーサーの上書き設定はそのまま ★★
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.cpp = {
install_info = {
url = "https://github.com/taku25/tree-sitter-unreal-cpp",
files = {"src/parser.c", "src/scanner.c"},
branch = "master",
},
filetype = "cpp",
}
require("nvim-treesitter.configs").setup(opts)
end,
},
そして、tree-sitter-unreal-cpp/after/queries/cpp/highlights.scm
には、Unreal Engine用のハイライト定義を記述します。
;; extends
; Unreal Engine macros
; GENERATED_BODY()
(unreal_body_macro "GENERATED_BODY" @function.macro)
(unreal_body_macro ["(" ")"] @punctuation.special)
; Macro heads as attributes (capture only the macro name token)
(uclass_macro "UCLASS" @attribute)
(ustruct_macro "USTRUCT" @attribute)
(uenum_macro "UENUM" @attribute)
(uproperty_macro "UPROPERTY" @attribute)
(ufunction_macro "UFUNCTION" @attribute)
; Parentheses of macros
(uclass_macro ["(" ")"] @punctuation.special)
(ustruct_macro ["(" ")"] @punctuation.special)
(uenum_macro ["(" ")"] @punctuation.special)
(uproperty_macro ["(" ")"] @punctuation.special)
(ufunction_macro ["(" ")"] @punctuation.special)
; Unreal API specifier (e.g. MYPROJECT_API)
(unreal_api_specifier (identifier) @type.qualifier)
; Unreal specifiers
(unreal_specifier (unreal_specifier_keyword) @variable.parameter)
(unreal_specifier (identifier) @variable.parameter) ; allow non-keyword identifiers as specifiers too
; key = "value" (direct)
(unreal_specifier
key: (unreal_specifier_keyword) @variable.parameter
value: (string_literal) @string)
(unreal_specifier
key: (identifier) @variable.parameter
value: (string_literal) @string)
; key = number / true / false / identifier
(unreal_specifier value: (number_literal) @number)
(unreal_specifier value: (true) @boolean)
(unreal_specifier value: (false) @boolean)
(unreal_specifier value: (identifier) @constant)
; meta=(DisplayName="...") and similar parenthesized assignments
(unreal_specifier
key: (unreal_specifier_keyword) @property
value: (parenthesized_expression
(assignment_expression
left: (identifier) @property
right: (string_literal) @string)))
この設定によって、無事にUnreal Engineの構文にも色がつくようになりました! 🎉
まとめ
もし新しい言語対応やライブラリ専用の構文のためにtree-sitterパーサーを自作し、「構文木は正しく解析できているのにハイライトが付かない!」という問題に直面したら、nvim-treesitter
のハイライトクエリの読み込み仕様を思い出してください。
解決の鍵は、after/queries/{filetype}/highlights.scm
にハイライト定義を記述し、そのパーサーリポジトリ自体をプラグインとしてNeovimに読み込ませることです。
この記事が、同じように困っている誰かの助けになれば嬉しいです。
追伸
今回この記事を書くにあたり、主に Gemini 2.5 Pro (deep research) と Github Copilot を利用しました。