Skip to main content
nvim-treesitter provides queries that enable four major editor features. While these are not automatically enabled, they offer significant improvements over Vim’s traditional regex-based approaches.
These features are not automatically enabled. You must explicitly configure them for each filetype or globally.

Syntax highlighting

Tree-sitter highlighting is more accurate and maintainable than regex-based approaches because it’s based on the actual syntax tree structure.

How it works

Highlighting works in three steps:
  1. Parse: The tree-sitter parser creates a syntax tree
  2. Query: highlights.scm matches tree nodes to capture names
  3. Highlight: Capture names map to highlight groups in your colorscheme
; From highlights.scm
(function_definition
  name: (identifier) @function)

(string) @string

"return" @keyword.return

Enabling highlighting

Create ~/.config/nvim/ftplugin/python.lua:
vim.treesitter.start()
This enables highlighting for Python files only.
Tree-sitter highlighting is provided by Neovim core, not nvim-treesitter. nvim-treesitter only provides the query files.

Highlight groups

Tree-sitter uses semantic capture names that map to highlight groups:
CaptureHighlight GroupDescription
@keyword.return@keyword.returnReturn statements
@function@functionFunction definitions
@function.call@function.callFunction calls
@variable@variableVariable names
@string@stringString literals
@comment@commentComments
@type@typeType names
Use :Inspect to see which highlight groups are applied at the cursor position.

Inspecting highlights

Debug highlighting issues:
:Inspect              " Show highlight groups at cursor
:InspectTree          " View syntax tree
:EditQuery highlights " Edit and test highlight queries

Highlight priority

When multiple captures overlap, priority determines precedence:
; Default priority: 100
(string) @string

; Higher priority wins
((string) @string.special
  (#lua-match? @string.special "^%$")
  (#set! priority 110))
Only set priorities between 90-120 to avoid conflicts with diagnostics (110) and LSP semantic tokens (125).

Code folding

Tree-sitter folding is based on the syntax tree structure, making it more accurate than indent-based or marker-based folding.

Enabling folds

-- In ftplugin or FileType autocommand
vim.wo[0][0].foldmethod = 'expr'
vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()'
You may also want to set foldlevel and foldnestmax for better default behavior:
vim.wo[0][0].foldlevel = 99    -- start with all folds open
vim.wo[0][0].foldnestmax = 3   -- limit fold depth

Defining folds

The folds.scm query marks foldable nodes:
; Functions can be folded
(function_definition) @fold

; Classes can be folded
(class_definition) @fold

; Control flow blocks can be folded
[
  (if_statement)
  (for_statement)
  (while_statement)
] @fold

; Grouping constructs can be folded
[
  (object)
  (array)
  (block)
] @fold

What should be foldable?

  • Function and method definitions
  • Class and interface definitions
  • Control flow statements (if/while/for)
  • Blocks with clear delimiters
  • Arrays and objects
  • Import statements (consecutive)
  • Line comments (consecutive)
  • Documentation blocks

Fold commands

zo  " Open fold under cursor
zc  " Close fold under cursor
za  " Toggle fold under cursor
zR  " Open all folds
zM  " Close all folds

Indentation

Tree-sitter indentation is experimental and may have breaking changes in future versions.
Tree-sitter indentation uses the syntax tree to determine proper indentation levels, providing more intelligent indentation than simple indent-on-tab.

Enabling indentation

-- In ftplugin or FileType autocommand
vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
Note the specific quote usage: double quotes for the string, single quotes for the module name.

Indent captures

The indents.scm query defines indentation behavior:
Increase indentation for the next line:
(function_definition) @indent.begin
(if_statement) @indent.begin
(for_statement) @indent.begin
Optionally, add immediate indent even when block is empty:
((if_statement) @indent.begin
  (#set! indent.immediate 1))
Mark the end of an indented region:
("}" @indent.end)
("end" @indent.end)
Start dedented region on the same line:
[
  "else"
  "elif"
  "catch"
  "finally"
] @indent.branch
Dedent on the following line:
(return_statement) @indent.dedent
(break_statement) @indent.dedent
Align arguments/parameters:
((argument_list) @indent.align
  (#set! indent.open_delimiter "(")
  (#set! indent.close_delimiter ")"))
This handles both styles:
foo(a,
    b,
    c)

foo(
  a,
  b,
  c
)
Behave like Vim’s autoindent:
(comment) @indent.auto
; Don't change indent for these
(preprocessor) @indent.ignore

; Remove all indent
(module_declaration) @indent.zero

Indentation edge cases

Avoiding last-line clashes

For Python-style indentation where closing delimiter shouldn’t match next line:
(if_statement
  condition: (parenthesized_expression) @indent.align
  (#set! indent.open_delimiter "(")
  (#set! indent.close_delimiter ")")
  (#set! indent.avoid_last_matching_next 1))
This handles:
if (a > b and
    c < d):    # dedented
    pass       # indented block

Language injections

Injections enable syntax highlighting for embedded languages (SQL in strings, CSS in HTML, code in markdown).

How injections work

  1. Detect: injections.scm identifies embedded language regions
  2. Parse: A separate parser parses the embedded content
  3. Highlight: The embedded language’s queries provide highlighting
Injections are automatically enabled when you use tree-sitter highlighting. No additional setup required.

Injection types

Explicitly specify the language:
((function_call
  name: (identifier) @_func
  arguments: (arguments
    (string content: _ @injection.content)))
  (#eq? @_func "sql")
  (#set! injection.language "sql"))
Highlights SQL in: sql("SELECT * FROM users")

Real injection examples

Vim commands in Lua

((function_call
  name: (_) @_vimcmd
  arguments: (arguments
    (string content: _ @injection.content)))
  (#set! injection.language "vim")
  (#any-of? @_vimcmd "vim.cmd" "vim.api.nvim_command"))
Highlights:
vim.cmd([[
  highlight Normal guibg=#000000
  set number
]])

Tree-sitter queries in strings

(string
  content: _ @injection.content
  (#lua-match? @injection.content "^%s*;+%s?query")
  (#set! injection.language "query"))
Highlights:
local query = [[
  ;; query
  (function_definition
    name: (identifier) @function)
]]

HTML in JavaScript

((template_string) @injection.content
  (#set! injection.language "html")
  (#set! injection.combined))
Highlights template literals with HTML:
const html = `
  <div class="container">
    <h1>Hello</h1>
  </div>
`

Injection directives

; Combine adjacent regions
(#set! injection.combined)

; Include child nodes
(#set! injection.include-children)

; Adjust region boundaries
(#offset! @injection.content 0 1 0 -1)

Feature status by language

Different languages support different features. Check the supported languages table:
LanguageHighlightsFoldsIndentsInjectionsLocals
pythonHFIJL
rustHFIJL
javascriptHFIJL
luaHFIJL
markdownHFIJ-
See SUPPORTED_LANGUAGES.md for the complete feature matrix.

Combining features

A typical setup enables all features for your preferred languages:
vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'python', 'rust', 'javascript', 'lua' },
  callback = function()
    -- Highlighting
    vim.treesitter.start()
    
    -- Folding
    vim.wo[0][0].foldmethod = 'expr'
    vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()'
    vim.wo[0][0].foldlevel = 99
    
    -- Indentation (experimental)
    vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
  end,
})

Troubleshooting features

Highlighting doesn’t work

  1. Check parser is installed: :checkhealth treesitter
  2. Verify queries exist: :echo stdpath('data') . '/site/queries/{lang}'
  3. Inspect tree: :InspectTree
  4. Check highlights: :Inspect

Folding is incorrect

  1. Verify fold query exists: runtime/queries/{lang}/folds.scm
  2. Check folding is enabled: :set foldmethod? foldexpr?
  3. Test fold query: :EditQuery folds

Indentation issues

Indentation is experimental. Report issues to nvim-treesitter/nvim-treesitter.
  1. Check indent query exists: runtime/queries/{lang}/indents.scm
  2. Verify indentexpr is set correctly
  3. Test with :IndentBlanklineToggle if using indent-blankline.nvim

Injections don’t highlight

  1. Ensure embedded language parser is installed
  2. Check injection query: :EditQuery injections
  3. Verify language name matches parser name

Performance considerations

Tree-sitter features are generally fast, but can impact performance on very large files:
  • Highlighting: Incremental and fast for files < 10K lines
  • Folding: Calculated once when opening file
  • Indentation: Computed per line during editing
  • Injections: Adds overhead for embedded languages
For files > 50K lines, consider disabling tree-sitter features:
if vim.fn.line('$') > 50000 then
  vim.treesitter.stop()
end

Next steps

Deepen your understanding:
  • Parsers - How parsers are installed and managed
  • Queries - Deep dive into the query language