Find definitions across your project, even without a language server.
treetags.nvim builds a ctags-style definition index using Tree-sitter and
SQLite. Browse project symbols, search by name, or look up the word under your
cursor with fzf-lua or
Telescope. No ctags executable
or tags file required.
Use it to explore unfamiliar code, navigate projects without a configured LSP, or search definitions across languages with one picker.
This is name-based navigation, not semantic resolution. A name can match
multiple definitions; treetags does not replace LSP references or rename.
Language coverage and definition kinds depend on the installed Tree-sitter
parsers and their locals queries.
Requirements | Installation | Quickstart | Indexing | Pickers | API | Advanced usage | Troubleshooting
- Neovim built with LuaJIT, with
vim.system,vim.fs.joinpath, and the Tree-sitter APIs used by the plugin. CI tracks Neovim's stable release rather than testing a fixed minimum version. - fd, available as
fdon yourPATH. - A Tree-sitter parser and a
locals.scmquery for each language you want to index. nvim-treesitter provides parser installation and these queries, but is not a runtime requirement: treetags uses Neovim's built-in Tree-sitter APIs. Parsers and queries installed another way should also work, provided Neovim can load them, with queries available underqueries/<parser_name>/locals.scmonruntimepath. - sqlite.lua and a loadable native SQLite library. Follow its platform-specific installation instructions.
- Optional: fzf-lua or Telescope, including the chosen picker's own dependencies. Neither is needed to use the Lua API.
Add this plugin spec to your lazy.nvim configuration:
{
'leblocks/treetags.nvim',
lazy = false,
dependencies = {
'kkharji/sqlite.lua',
'nvim-treesitter/nvim-treesitter', -- Omit if you provide parsers and locals.scm queries another way.
'ibhagwan/fzf-lua', -- Optional; used in the quickstart below.
},
}There is no setup() function. Do not add opts = {} or config = true
to the treetags spec: lazy.nvim would try to call one. Installing the plugin
does not build an index.
Install parsers for the languages you want to index using nvim-treesitter or your preferred method. Installing treetags does not install parsers for you.
If you prefer Telescope, replace the fzf-lua dependency with:
{
'nvim-telescope/telescope.nvim',
dependencies = { 'nvim-lua/plenary.nvim' },
}Meet the chosen picker's upstream Neovim and external-tool requirements.
In particular, fzf-lua needs an fzf or skim executable.
With Neovim 0.12+, add this to your init.lua:
vim.pack.add({
{ src = 'https://github.com/kkharji/sqlite.lua' },
{
src = 'https://github.com/nvim-treesitter/nvim-treesitter',
version = 'main',
},
{ src = 'https://github.com/ibhagwan/fzf-lua' }, -- Optional; used in the quickstart below.
{ src = 'https://github.com/leblocks/treetags.nvim' },
})This requires Git on your PATH. The plugins are available after
vim.pack.add() returns; treetags needs no setup() call.
Follow the nvim-treesitter installation instructions
to install parsers for your languages. Omit that entry if you already provide
parsers and locals.scm queries another way.
To use Telescope instead, replace the fzf-lua entry with entries for
https://github.com/nvim-lua/plenary.nvim and
https://github.com/nvim-telescope/telescope.nvim.
The native SQLite library and chosen picker's external tools must still be
installed separately.
Start Neovim in your project root. Before the first treetags API call, use
:pwd to confirm the working directory or :cd to change it.
Build the index:
:lua require('treetags').init_database()Wait for the finished processing files notification, then open a picker:
:lua require('treetags').get_pickers('fzf').treetags_all()Add these suggested mappings to your Lua configuration after the plugins load.
Use 'telescope' instead of 'fzf' if you installed Telescope:
local pickers = require('treetags').get_pickers('fzf')
vim.keymap.set('n', '<leader>ta', pickers.treetags_all, { desc = 'Treetags: all definitions' })
vim.keymap.set('n', '<leader>tw', pickers.treetags_cword, { desc = 'Treetags: word under cursor' })
vim.keymap.set('n', '<leader>tW', pickers.treetags_cWORD, { desc = 'Treetags: WORD under cursor' })
vim.keymap.set('n', '<leader>tl', pickers.treetags_live, { desc = 'Treetags: live search' })Run init_database() again when you want to refresh results after source
changes. Indexing is manual; the plugin does not watch files or update on save.
For in-editor documentation, see :help treetags.txt. Run
:checkhealth treetags to check fd, SQLite, and optional picker availability.
File discovery runs fd --type file in Neovim's current working directory.
The normal fd hidden-file and ignore rules apply. Files need a recognized
filetype, an installed parser, and a locals query to produce definitions.
The plugin does not install missing parsers.
Calling init_database() clears the active index and schedules parsing work.
While indexing, guarded public operations notify and return without querying;
they do not wait or queue a retry.
These are the defaults:
require('treetags').init_database({
extensions = {},
kinds_filter = {},
buffer_size = 1000,
incremental = false,
})| Option | Meaning |
|---|---|
extensions |
Allow-list of file extensions, without leading dots. Empty means consider every file returned by fd. |
kinds_filter |
Deny-list of definition kinds. Empty means store every captured kind. |
buffer_size |
Number of definitions accumulated before a batch insert. |
incremental |
When true, reparse changed Git-tracked files since the previous index instead of scanning all files. Defaults to false. |
Incremental indexing uses the most recently recorded Git commit as its baseline.
On the first incremental run, or when no Git history is available, it indexes
all files. Later runs remove and reparse changed files, and remove deleted
files from the index. Untracked files are not included by Git diff. Keep the
same extensions and kinds_filter between incremental runs; use a full
rebuild after changing those options.
require('treetags').init_database({ incremental = true })For example, index only Lua, JavaScript, and C# files, excluding var captures:
require('treetags').init_database({
extensions = { 'lua', 'js', 'cs' },
kinds_filter = { 'var' },
})Kinds come from Tree-sitter capture names: local.definition.function becomes
function, while bare local.definition becomes definition. Available kinds
vary by language and query; they are not a fixed set of LSP symbol kinds.
The database filename is derived from the working directory when the SQLite
connection is first opened. Databases are stored under stdpath('state') and
can be reused across Neovim sessions.
Keep the same project working directory for indexing and navigation.
The connection is cached for the session, so changing :cd does not switch
an already opened database. Open a new Neovim session for a different project.
Stored paths are relative to the project directory.
Both integrations expose the same functions through
require('treetags').get_pickers('fzf') or get_pickers('telescope').
Only the requested integration is loaded.
| Function | Results |
|---|---|
treetags_all() |
Every indexed definition, filtered interactively by the picker. |
treetags_cword() |
Exact name matches for <cword> under the cursor. |
treetags_cWORD() |
Exact name matches for <cWORD> under the cursor. |
treetags_live() |
Database lookup as the prompt changes; an empty prompt shows no results. |
Live lookup uses case-sensitive SQLite GLOB matching with * around the
prompt. GLOB metacharacters such as *, ?, and bracket expressions retain
their pattern meaning. This is not a literal substring search or a fuzzy
database search.
The integration provides file previews, multi-selection, and these actions:
| Key | Action |
|---|---|
Enter |
Open the selected definition. |
Ctrl-V |
Open in a vertical split. |
Ctrl-S |
Open in a horizontal split. |
Ctrl-T |
Open in a new tab. |
Alt-Q |
Send selected definitions to the quickfix list. |
Ctrl-G |
Toggle between live database search and fuzzy filtering of the current query. |
The integration uses Telescope's configured file previewer, generic sorter,
mappings, and actions. Split/tab navigation, multi-selection, and quickfix
export follow your Telescope configuration, not the fzf-specific bindings above.
This is a direct Lua integration; no load_extension('treetags') call is needed.
Each picker accepts an optional Telescope options table:
local pickers = require('treetags').get_pickers('telescope')
vim.keymap.set('n', '<leader>ta', function()
pickers.treetags_all({ layout_strategy = 'vertical' })
end, { desc = 'Treetags: all definitions' })All public functions are available through require('treetags'):
| Function | Purpose |
|---|---|
init_database(params) |
Clear and asynchronously rebuild the index; params is optional. |
get_all_treetags() |
Return all indexed definitions. |
get_treetag_by_name(name) |
Return exact name matches. |
get_treetag_by_name_glob(name) |
Return case-sensitive GLOB matches using *name*. |
get_pickers(type) |
Return the 'fzf' or 'telescope' integration. |
get_db() |
Return the underlying sqlite.lua database object. |
After indexing finishes, lookup functions return arrays of records shaped like:
{
name = 'MyFunction',
kind = 'function',
path = 'lua/example.lua',
row = 10,
column = 7,
}row and column are one-based. Paths are normalized relative paths.
Guarded calls return nil instead of results while indexing is in progress.
Use get_db() for queries beyond the built-in lookups:
local db = require('treetags').get_db()
local methods = db:eval([[
SELECT d.name, f.path, d.row, d.column
FROM definitions d
JOIN files f ON f.id = d.path_id
WHERE d.kind = 'method'
ORDER BY d.name
]])Run this only after indexing finishes. The database schema is an implementation detail and may change between releases.
Run from the project directory with a Neovim configuration that loads treetags and its dependencies. Wait for scheduled work before quitting:
nvim --headless -c "lua require('treetags').init_database()" -c "lua if not vim.wait(600000, function() return not vim.g.treetags_processing end) then vim.cmd('cquit') end" -c "qall"The command exits with an error if the wait times out after ten minutes. The processing flag indicates completion, not that every file was indexed; inspect notifications for missing parsers or queries.
Start with :checkhealth treetags and :messages.
| Symptom | What to check |
|---|---|
| fd cannot be found | Ensure the executable is named fd and is on Neovim's PATH. Some distributions install it as fdfind. |
| SQLite fails to load | Install the native SQLite library and follow sqlite.lua's platform instructions. On Windows, set vim.g.sqlite_clib_path to the SQLite DLL before loading treetags. |
| Files or languages are missing | Check :pwd, fd ignore rules, extensions, and kinds_filter. Install the language parser and confirm a locals query exists; syntax highlighting alone does not establish this. |
| Results are stale | Run init_database({ incremental = true }) to update Git-tracked changes, or init_database() for a full rebuild. |
| Navigation opens the wrong path | Keep the working directory at the indexed project root. Restart Neovim when switching projects; the database connection is cached. |
| Live search has no results | Enter a nonempty prompt, check identifier casing, and remember that GLOB metacharacters are active. |
| Operations keep reporting that indexing is running | Inspect :messages for errors; if indexing stopped without a completion notification, restart Neovim before retrying. |
The health check does not verify per-language parser or locals query coverage.
treetags.nvim is the successor to
hopcsharp.nvim, generalized from
C# navigation to languages with suitable Tree-sitter parsers and queries.
Planned: additional picker integrations.
See LICENSE for the GNU General Public License v3.0.
All documentation in this repo was prepared with assistance from AI.
