Neovim incremental selection using Tree-sitter
I use incremental selections in Neovim all the time. This is where I tap, tap, tap, and on every single tap, the selection expands starting from the cursor position and climbs up by the node or the whole scope. This feature uses Tree-sitter under the hood, so it respects the grammar of the programming language. One of my favourite features of Neovim!
Configure nvim-treesitter
To enable this feature, you need to install nvim-treesitter first. The configuration differs depending on the branch you’re using, so I will provide a recipe for the master (which is still the default branch at the time of writing this post) and the main branch, which will eventually become the default one according to the roadmap. I use <A-o> and <A-i> where “o” and “i” work as a mnemonics for “select out” and “select in”.
The master branch
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<A-o>",
node_incremental = "<A-o>",
scope_incremental = "<A-O>",
node_decremental = "<A-i>",
},
},
})
end,
}
The main branch
One of the biggest changes in the main branch is the lack of a modules framework, so some of the stuff like indentation, folding, and also incremental selection need to be recreated. The treesitter-modules.nvim is the easiest and the most reliable plugin to reproduce missing functionalities amongst the other ones I tested. Also, keep in mind that using the main branch requires the tree-sitter-cli, so make sure you have one in your path.
return {
{
"nvim-treesitter/nvim-treesitter",
branch = "main",
build = ":TSUpdate",
},
{
{
"MeanderingProgrammer/treesitter-modules.nvim",
dependencies = { "nvim-treesitter/nvim-treesitter" },
opts = {
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<A-o>",
node_incremental = "<A-o>",
scope_incremental = "<A-O>",
node_decremental = "<A-i>",
},
},
},
},
},
}
Built-in incremental selection
The good news is that LSP-based incremental selection is coming to the Neovim core soon. The an and in in visual mode will map to the outer and inner incremental selections. Here are the new default mappings.
vim.keymap.set('x', 'an', function()
vim.lsp.buf.selection_range('outer')
end, { desc = "vim.lsp.buf.selection_range('outer')" })
vim.keymap.set('x', 'in', function()
vim.lsp.buf.selection_range('inner')
end, { desc = "vim.lsp.buf.selection_range('inner')" })
There is also a strong signal from core maintainers that the LSP implementation will eventually be replaced with the Tree-sitter one. Nice 👌
I know that this feature is not specific to Neovim. Helix also comes with expand_selection and shrink_selection, also powered by Tree-sitter. Visual Studio Code most likely uses TextMate grammar and LSP under the hood to create the Expand and Shrink commands. IntelliJ uses its own proprietary PSI (Program Structure Interface) to enable Extend Selection and Shrink Selection, but I have never used it and I don’t know how it compares to the TS one.
Done 👋
Update 2026.03.09
The incremental selection using Treesitter is merged with the master branch now and it is expected to land on Neovim 0.12. This update kills the previous implementation, will use Treesitter via new keybindings, and fall back to the LSP-based one only when Treesitter is not set up. Here are the new updated mappings.
vim.keymap.set({ 'x' }, '[n', function()
require 'vim.treesitter._select'.select_prev(vim.v.count1)
end, { desc = 'Select previous treesitter node' })
vim.keymap.set({ 'x' }, ']n', function()
require 'vim.treesitter._select'.select_next(vim.v.count1)
end, { desc = 'Select next treesitter node' })
vim.keymap.set({ 'x', 'o' }, 'an', function()
if vim.treesitter.get_parser(nil, nil, { error = false }) then
require 'vim.treesitter._select'.select_parent(vim.v.count1)
else
vim.lsp.buf.selection_range(vim.v.count1)
end
end, { desc = 'Select parent treesitter node or outer incremental lsp selections' })
vim.keymap.set({ 'x', 'o' }, 'in', function()
if vim.treesitter.get_parser(nil, nil, { error = false }) then
require 'vim.treesitter._select'.select_child(vim.v.count1)
else
vim.lsp.buf.selection_range(-vim.v.count1)
end
end, { desc = 'Select child treesitter node or inner incremental lsp selections' })
Man, you saved my life! :)
I'm glad it helped you out luski 🤗
Your post helped me a lot! Possibly, some of my neovim config files can give you some ideas. https://codeberg.org/sergio_araujo/my-lazy-nvim
Where can I find your neovim config?
Hey, thanks for sharing. My Neovim config is here. I hope you can find some interesting bits there.
i use flash.nvim for this, you enter treesitter mode
Sand then;to expand selectionThats one of the popular solutions, yeah. Plenty of people prefer that. I tried flash before but this workflow is not for me.
This post actually motivated me to delete flash.nvim and replace its treesitter expand selection with this. Default keybindings are a bit weird gnn to init selection then grn to increment node and grm to decrement. Maybe ill use your shortcuts.
#Minimalism as they say it.
I'm glad this post inspired you to try something new. Yeah, there is something cool about minimal nvim config. The small the config, the less maintenance.
Replacing a whole plugin with this snippet, tysm Pawel
Having default keybinds OOTB is great. I knew #36993 was merged but had no idea the PR included keybinds too, thanks!