Tree-sitter
Tree-sitter gives NeoVim real parsing of source code, replacing regex-based syntax highlighting. It also enables structural navigation, smart folding, and query-based text objects.
Enabling Highlighting
Tree-sitter highlighting is built into NeoVim. As of 0.12, it’s enabled by default for Markdown. For other languages, enable it per-filetype or globally:
-- Enable for specific languages
vim.api.nvim_create_autocmd("FileType", {
pattern = { "lua", "python", "rust", "go", "javascript", "typescript" },
callback = function()
vim.treesitter.start()
end,
})
-- Or enable for ALL filetypes that have a parser installed
vim.api.nvim_create_autocmd("FileType", {
callback = function()
-- Only start if a parser exists for this filetype
pcall(vim.treesitter.start)
end,
})
Gotcha:
vim.treesitter.start()requires a parser for the language to be installed. Without one, it silently does nothing (or errors if called directly). Install parsers first.
Installing Parsers
NeoVim bundles parsers for a few languages (C, Lua, Markdown, Vimdoc, query). For everything else, use nvim-treesitter:
-- Using vim.pack (0.12+)
vim.pack.add("nvim-treesitter/nvim-treesitter")
-- Then install parsers
-- :TSInstall lua python rust go typescript
Or compile parsers manually (the nvim-treesitter plugin automates this):
# Parsers live in ~/.local/share/nvim/site/parser/
# Each is a .so file compiled from a tree-sitter grammar
Folding
Tree-sitter provides structural folding - fold functions, classes, blocks based on the parse tree:
-- Use tree-sitter for folding
vim.o.foldmethod = "expr"
vim.o.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.o.foldlevel = 99 -- Start with all folds open
vim.o.foldlevelstart = 99
With this, zc closes the current function/class/block, zo opens it, za toggles. The folds follow code structure, not indentation.
Tip: Set
foldlevel = 99to start with everything unfolded. Otherwise NeoVim opens files with all folds closed, which is disorienting.
Inspecting the Parse Tree
NeoVim includes a tree inspector for debugging:
:InspectTree " Opens a split showing the parse tree
:EditQuery " Opens a query editor with live preview
:Inspect " Shows highlight groups under cursor
:InspectTree is invaluable when writing queries or debugging why highlighting looks wrong. The tree updates live as you edit.
Queries
Queries are S-expression patterns that match tree-sitter nodes. NeoVim uses them for highlighting, folding, indentation, and text objects.
;; Example: highlight query for Lua
;; Matches function calls and captures them as @function.call
(function_call name: (identifier) @function.call)
;; Match string literals
(string) @string
;; Match comments
(comment) @comment
Query files live in runtime/queries/<lang>/:
| File | Purpose |
|---|---|
highlights.scm | Syntax highlighting captures |
folds.scm | Foldable regions |
indents.scm | Indentation rules |
injections.scm | Language injection (e.g., SQL in strings) |
locals.scm | Scope and reference tracking |
Using Queries in Lua
-- Parse a query
local query = vim.treesitter.query.parse("lua", [[
(function_declaration
name: (identifier) @func_name)
]])
-- Get the parser for the current buffer
local parser = vim.treesitter.get_parser(0, "lua")
local tree = parser:parse()[1]
local root = tree:root()
-- Iterate matches
for id, node, metadata in query:iter_captures(root, 0) do
local name = query.captures[id] -- capture name (e.g., "func_name")
local text = vim.treesitter.get_node_text(node, 0)
print(name .. ": " .. text)
end
Text Objects with Tree-sitter
With nvim-treesitter-textobjects (community plugin), you get structural text objects:
-- After installing nvim-treesitter-textobjects
-- Select inner/outer functions, classes, arguments:
-- vaf - select around function
-- vif - select inner function
-- vac - select around class
-- ]m - jump to next method
-- [m - jump to previous method
Without the plugin, you can use the built-in node selection:
-- Select the tree-sitter node under cursor, expanding with repeated presses
vim.keymap.set({ "n", "x" }, "<leader>v", function()
vim.treesitter.incremental_selection.init_selection()
end)
Playground: Quick Exploration
A fast way to understand what tree-sitter sees:
-- Get the node under cursor
local node = vim.treesitter.get_node()
print(node:type()) -- e.g., "function_declaration"
print(node:range()) -- start_row, start_col, end_row, end_col
print(vim.treesitter.get_node_text(node, 0)) -- the actual text
-- Walk up the tree
local parent = node:parent()
print(parent:type()) -- e.g., "chunk" (top-level in Lua)
-- Get named children
for child in node:iter_children() do
if child:named() then
print(child:type(), vim.treesitter.get_node_text(child, 0))
end
end