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 を利用しました。
