Configuring Neovim treesitter from first principles

June 30, 2026


In April I wrote a migration guide for updating nvim-treesitter to Neovim 0.12. Ironically enough, a day after that post went up, the nvim-treesitter plugin maintainers archived it.

The plugin still works, but it's frozen, and watching it break my setup once already made me want to understand what sits underneath it. Neovim 0.12 ships almost everything you need built-in, so you can run treesitter with no plugin at all.

New to treesitter?

It parses your code into a syntax tree so Neovim understands structure instead of guessing with regex. That tree powers highlighting, folding, and navigation. My migration post has a longer explainer. There is also this playground that lets you watch the tree build as you type.

nvim-treesitter is really a manager sitting on top of three layers: parsers, queries, and a bit of Lua. Once you see the layers, the plugin becomes optional.

1. Parsers

A parser is a small compiled library, one per language, that turns source text into a syntax tree. lua.so, python.so, and so on. (A syntax tree, or formally a parse tree, is a data structure that represents how a program or snippet is organized.)

Neovim 0.12 bundles seven of them: c, lua, markdown, markdown_inline, query, vim, and vimdoc. This is only enough to edit your own config, so for any other language, you compile the parser yourself.

First, install the tree-sitter CLI, clone the language's grammar repo, and build it. Below is JSON as an example, but the pattern is the same for any language: clone tree-sitter-<lang> and build it to <lang>.so.

git clone https://github.com/tree-sitter/tree-sitter-json
cd tree-sitter-json
tree-sitter build -o ~/.config/nvim/parser/json.so

The clone URL isn't a fixed template, though. JSON lives in the official tree-sitter org, but many grammars sit elsewhere (tree-sitter-grammars/... or a language's own org), so search for the repo first.

Neovim searches for parsers as parser/{lang}.* in every directory on its runtimepath, so dropping json.so into ~/.config/nvim/parser/ is enough for it to find and load.

2. Queries

A parser gives you a syntax tree. Queries decide what to do with it. A query is an S-expression pattern that matches nodes and tags them with capture names like @keyword, @string, or @comment. It's like regex, but for the syntax tree instead of raw text. Neovim maps those captures to highlight groups, and your colorscheme colors them.

Queries live as .scm files under a queries/<lang>/ directory on the runtimepath, one file per job:

  • queries/<lang>/highlights.scm for highlighting
  • queries/<lang>/folds.scm for folding
  • queries/<lang>/injections.scm for embedded languages

For the built-in seven languages, Neovim already bundles their queries along with the parsers. For anything else, drop your own into ~/.config/nvim/queries/<lang>/. The big query corpus that nvim-treesitter shipped for hundreds of languages lives in its repo, so you can copy the ones you need into your config. One catch: your file replaces the bundled queries for that language (first on runtimepath wins), so prepend ; extends to it if you mean to add to them instead.

3. Putting it together

Now wire it together. Run this to start highlighting:

vim.treesitter.start()

With no arguments, it attaches the highlighter to the current buffer using the language for its filetype. It also turns off the old regex syntax highlighting. Re-enable that with vim.bo.syntax = 'ON' if some plugin still needs it.

To auto-run this everywhere, put the call in a FileType autocommand:

vim.api.nvim_create_autocmd('FileType', {
  callback = function(args)
    pcall(vim.treesitter.start, args.buf)
  end,
})

The pcall swallows the error for filetypes with no parser installed. For folding, set two options:

vim.o.foldmethod = 'expr'
vim.o.foldexpr = 'v:lua.vim.treesitter.foldexpr()'

Unfortunately, Neovim doesn't provide indentation out of the box. To set it up, find it in the archived nvim-treesitter code, although note that it's still experimental. For most languages, I think Neovim's built-in indent rules are good enough.

When to skip the plugin

Just because you can doesn't mean you should. Going plugin-free, you give up things like :TSInstall, automatic parser updates, the giant query corpus, and textobjects like vaf to select a function. If you use those, keep the plugin. Its main branch still does this job well and now calls these same core APIs under the hood.

But by setting up treesitter yourself, you get something with no moving parts that can break. No plugin rewrite forcing a migration on you. Nothing to update. For a handful of languages with plain highlighting and folding, the whole thing is a parser file, a few queries, and ten lines of Lua.