Title: How I Adapted My Custom Parser for the New nvim-treesitter `main` Branch
Introduction
I’m the author of tree-sitter-unreal-cpp
, a custom Tree-sitter parser for Unreal Engine’s C++ dialect. Recently, I decided to update my Neovim setup to the bleeding-edge main
branch of nvim-treesitter
. I expected a few hiccups, but what I got was a complete system failure: all my carefully crafted syntax highlighting for Unreal Engine macros had vanished. 😱
What followed was a deep dive into the new nvim-treesitter
architecture. This post documents that debugging journey and the key discoveries I made, which culminated in a successful migration. If you’re a plugin author or use custom parsers, I hope my experience can save you some time.
The Debugging Journey
My goal was to get my custom highlights for macros like UCLASS
and specifiers like Blueprintable
working again. I started by systematically checking each part of the chain.
1. Verifying the Query File
First, I had to confirm if my custom highlights.scm
file was even being loaded. On the main
branch, the debugging tools have changed, and the most reliable way is to use Neovim’s built-in API directly.
I ran this command in Neovim:
:lua print(vim.inspect(vim.treesitter.query.get_files(vim.bo.filetype, "highlights")))
The output confirmed that my custom query file (.../queries/cpp/highlights_unreal.scm
) was indeed on the list. So, the file was being found. Step one: success.
2. Inspecting the Applied Highlights
Next, I placed my cursor over a keyword that should have been highlighted, like Blueprintable
, and used the :Inspect
command to see which syntax groups were being applied.
The result? Nothing. Or rather, nothing from Tree-sitter. It was clear that even though my parser was working (which I confirmed with :InspectTree
), no highlighting rules from my .scm
file were being applied.
3. The Deepening Mystery
This was the confusing part. My parser was correct, my query file was loaded, and its syntax was valid according to the latest Neovim documentation. Why was there no connection between the two?
I started to suspect something more fundamental had changed. That’s when I decided to re-read the nvim-treesitter
main
branch README from top to bottom.
The Breakthrough: It’s a Whole New Plugin
The README revealed the truth: the main
branch isn’t an update; it’s a complete, incompatible rewrite.
My old configuration was fundamentally wrong because the plugin’s core philosophy has changed. Here are the key takeaways:
-
setup
is minimal: The hugesetup
table where you used to configure everything (highlight
,indent
,ensure_installed
) is gone. -
ensure_installed
is gone: You no longer provide a list of parsers to automatically install. Instead, you must explicitly call a function to install them. -
Feature activation is now manual: Switches like
highlight = { enable = true }
have been removed. The plugin no longer automatically activates features. It simply provides the parsers and queries; it is now the user’s responsibility to enable highlighting, indentation, and folding using Neovim’s core APIs.
nvim-treesitter
has evolved from an all-in-one framework into a more focused parser manager. The “power switch” for highlighting was off because I was the one who had to turn it on.
The Final Configuration
Armed with this new understanding, I wrote my final configuration. This code now correctly reflects the new, explicit philosophy of the main
branch.
Here is my setup using lazy.nvim
:
-- lazy.nvim spec
-- lazy.nvim spec in your plugins file (e.g., lua/plugins/treesitter.lua)
return {
{
'nvim-treesitter/nvim-treesitter',
branch = 'main',
-- The build step is crucial to automatically install/update parsers
build = ':TSUpdate',
-- This plugin depends on the custom parser being available
dependencies = {
'taku25/tree-sitter-unreal-cpp',
},
config = function()
-- === 1. Configure nvim-treesitter to use our custom parser ===
-- We hook into the 'TSUpdate' event to override the parser config for "cpp".
-- This ensures that whenever `:TSUpdate` runs, it knows to use your repo.
vim.api.nvim_create_autocmd('User', {
pattern = 'TSUpdate',
callback = function()
-- Override the 'cpp' parser definition
require('nvim-treesitter.parsers').cpp = {
install_info = {
url = 'https://github.com/taku25/tree-sitter-unreal-cpp',
-- Pinning to a specific commit is a good practice for stability
revision = 'd5673330c80033dfbf6ac868c7bbbfb16d53b5f6',
},
-- Important: Disabling the default C++ parser is necessary
-- if your custom parser doesn't cover all standard C++ syntax.
-- If your parser fully supports standard C++, you can remove this.
--
-- By setting this, the default C++ parser will still be used
-- for highlighting, and your custom parser will be layered on top
-- via your `;; extends` query file.
-- maintainers = {},
-- files = {}
}
end
})
-- === 2. Activate features for specific languages ===
-- This is the new, correct way to enable highlighting and indentation.
local langs = { "c", "cpp", "c_sharp", "lua", "vim", "vimdoc", "h" }
local group = vim.api.nvim_create_augroup('MyTreesitterSetup', { clear = true })
vim.api.nvim_create_autocmd('FileType', {
group = group,
pattern = langs,
callback = function(args)
-- Enable highlighting for the buffer
vim.treesitter.start(args.buf)
-- Enable indentation for the buffer
vim.bo[args.buf].indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
end,
})
end,
},
}
With this configuration in place, all my custom Unreal Engine highlights came back to life, coexisting peacefully with LSP semantic tokens. Victory! 🚀
Conclusion
Migrating to the nvim-treesitter
main
branch requires forgetting what you know about the old master
branch. The key is to understand the shift in responsibility: the plugin manages the tools (parsers and queries), but you, the user, now use Neovim’s APIs to decide when and how to use them.
Always read the README, especially on a main
branch! I hope this write-up saves someone else from a similar debugging headache. Happy coding!