Skip to main content

Command Palette

Search for a command to run...

Regex-Based Syntax Highlighting Had to Go

Updated
2 min readView as Markdown
S
I'm Sushanth, a high school senior in Hyderabad, India. This is where I write about the things I spend time on: learning from mistakes, building things in code, and the tools I actually work with day to day. I hold the Candidate Master title and a FIDE International Master norm, earned across roughly 140 rated tournaments in six countries. I stopped competing in 2025, but the habit competition taught me, sitting down after every loss and figuring out exactly where I went wrong, shapes how I approach everything else. On the technical side, I've gone deep on C/C++ through Duke University and University of London coursework. I'm also someone who likes a good terminal workflow and enjoys exploring (and abandoning) productivity tools. Expect posts on Neovim, tmux, what broke this week, and whatever I'm tinkering with.

A parser that knows exactly where a function ends, not a regex that's guessing — and everything downstream quietly depending on the difference.

For a long time I thought treesitter was a highlighting upgrade: nicer colors, a keyword that's the right shade of purple instead of whatever regex pattern happened to match it. That's true, but it's not the reason it's load-bearing in my config now. The reason is that everything downstream of it needs a real parse tree, not colors.

Here's the setup, which is smaller than I expected the first time I read it:

require("nvim-treesitter").setup()
require("nvim-treesitter").install({ "lua", "vim", "python", "java", "c", "cpp", ... })

vim.api.nvim_create_autocmd("FileType", {
  callback = function(ev)
    if pcall(vim.treesitter.start) then
      vim.bo[ev.buf].indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
    end
  end,
})

vim.treesitter.start() builds an actual syntax tree for the buffer: not "these characters look like a keyword," but "this node is a function_definition, this one's its body, this one's a parameter." The indentexpr line hands indentation decisions to that tree too, instead of Vim's regex-based indent heuristics guessing from the previous line.

That tree is what everything else queries. My comment in the textobjects plugin spells out the dependency more bluntly than I would've expected myself to write it:

nvim-treesitter's "main" rewrite moved the textobjects queries out of the core plugin, and nothing else on the runtimepath provides them — so without this, no plugin can ask "where does this function start and end." That includes mini.ai's treesitter spec, which is what wires up af/ic. Those mappings are dead without this plugin.

So af (a function) only works because treesitter-textobjects can answer a structural query, @function.outer, and hand back exact start/end coordinates. Same story for ]f / [f jumping between function starts, and ]k / [k for classes, both bound straight to that query system. A regex highlighter has no equivalent answer. It can guess that a line looks like the start of a function; it can't tell you, precisely, where that function's body actually closes.

The sticky-scope plugin makes the same point in scrolling form: it shows the enclosing function or class as you scroll past its opening line, because it's reading the same tree, not re-deriving scope from indentation.

More from this blog