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 huge setup 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!

Similar Posts