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 = 99 to 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>/:

FilePurpose
highlights.scmSyntax highlighting captures
folds.scmFoldable regions
indents.scmIndentation rules
injections.scmLanguage injection (e.g., SQL in strings)
locals.scmScope 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