Simplify your Neovim config navigation using Telescope.nvim and Tree-sitter
I was using Neovim as my editor for almost 2 years now. My config has evolved a lot since I started, so did my package management. Here I will share an easy way to navigate your growing config.
Shortly about my setup
I’m using Neovim together with iTerm2. I use fish as my main shell and rely on chezmoi to manage my dotfiles. My Neovim config is written in Lua, and I try to keep it as short as possible.
Previously, I used to have a dedicated file for every package configuration. However, I found it difficult to navigate, especially when trying out new packages and making changes. Therefore, I now store all my packages configuration in a single file located at $HOME/.config/nvim/lua/packages
.lua. This file contains 782 lines of code and 43 plugins. To manage my plugins, I use lazy.nvim, a package manager that is fast and features declarative configuration.
How I structure my packages.lua
file
To give you an idea of how I structure my packages.lua
file, here is an example:
-- ... code for initializing lazy.nvim
require("lazy").setup({
-- ... more packages
-- For some packages, I don't need any additional configuration:
"nvim-lua/popup.nvim",
-- But for others, I may need to configure them in more detail:
{
"stevearc/dressing.nvim",
dependencies = { "telescope.nvim" },
config = function()
require("dressing").setup({
select = {
telescope = require("telescope.themes").get_cursor({
layout_config = {
preview_height = nil,
},
}),
},
})
end,
},
-- ... more packages
}, {
-- ... additional lazy.nvim options
})
As you can see, my configuration is simple and straightforward. The lazy.nvim package manager helps me keep my packages organized and concise. While I haven't included specific configurations here, I may write about them in more detail in a future post.
About Telescope.nvim and Tree-sitter
Telescope.nvim is a great fuzzy finder Neovim package, similar to fzf, but written in Lua. It is very fast and works like it’s a part of neovim itself. It is easily extendable, and we will make use of that later.
Tree-sitter is a part of neovim right now. This is an incremental parser that generates CST (Concrete Syntax Tree) . It is very fast and powers neovim syntax highlighting, but what’s most important to us is the CST part.
Lets extract list of packages
My first implementation was using regex to extract a list of plugins, but that was non-reliable, as it depends on file formatting, and would suddenly pick up strings inside a bigger plugin configuration. And is that even fun, huh?
Let’s use Tree-sitter for this! After spending a few hours browsing through docs, I came up with this code:
local function get_list_of_packages()
-- Create a new buffer in the background and load packages.lua file
local bufnr = vim.api.nvim_create_buf(false, true)
local file_contents = vim.fn.readfile(PLUGINS_CONFIG_PATH)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, file_contents)
local root = vim.treesitter.get_parser(bufnr, "lua"):parse()[1]:root()
-- There is an alternation in the query so we can capture both table_constructor and string
local query = vim.treesitter.query.parse(
"lua",
[[
(function_call
name: (dot_index_expression) @require-name
(#eq? @require-name "require(\"lazy\").setup")
(arguments
(table_constructor
(field
[(table_constructor . (field (string) @package-name))
(string) @package-name]))))
]]
)
-- Table of { name, row }
local packages = {}
for id, node, _ in query:iter_captures(root, bufnr) do
local capture_name = query.captures[id]
-- Filter out "require-name" capture
if capture_name == "package-name" then
local start_row, start_col, end_row, end_col = node:range()
local package_name = vim.api.nvim_buf_get_text(bufnr, start_row, start_col, end_row, end_col, {})[1]
-- Remove quotes from string
package_name = string.sub(package_name, 2, -2)
table.insert(packages, { name = package_name, row = start_row + 1 })
end
end
-- Delete temp buffer
vim.api.nvim_buf_delete(bufnr, {})
return packages
end
This code creates a new buffer in the background and loads the packages.lua file. It then uses Tree-sitter to parse the buffer and extract the list of packages from it. The resulting table includes both the package name and its row number in the file.
While this Tree-sitter query works for my specific scenario, you can modify it to suit your own needs. To learn more about queries, consult the official documentation. Additionally, I highly recommend using the Tree-sitter playground package, which allows you to preview the results of your query in real-time.
Jump to package config using Telescope.nvim
We have a list of packages now, the next step is to jump to a specific config. We can achieve this by creating a custom picker for Telescope.nvim. I store this picker in a separate lua/custom_telescope.lua
file:
local M = {}
M.goto_package_config = function(opts)
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local packages = get_list_of_packages()
opts = opts or {}
pickers
.new(opts, {
prompt_title = "Plugin Config",
finder = finders.new_table({
results = packages,
entry_maker = function(entry)
return {
filename = PLUGINS_CONFIG_PATH,
display = entry.name,
ordinal = entry.name,
row = entry.row,
col = 0,
}
end,
}),
sorter = conf.generic_sorter(opts),
})
:find()
end
return M
That’s it! You can now call or bind this picker like this:
:lua require("custom_telescope").goto_package_config()
Here it is in action:
Conclusion
This was fun to explore syntax tree parsing, and I’m happy it turned out as a simple and functional tool I use every day now. When I’m in the middle of something, and I need to quickly change some package setting, I will just press my hotkey, and jump right to it!
A great read for neovim fans!