Skip to content
Theme:

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!

A demo of incremental selection in 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' })

Comments

  • l
    luski

    Man, you saved my life! :)

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Paweł Grzybek
      Paweł Grzybek

      I'm glad it helped you out luski 🤗

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • S
      Sérgio Araújo

      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?

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • Paweł Grzybek
        Paweł Grzybek

        Hey, thanks for sharing. My Neovim config is here. I hope you can find some interesting bits there.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • b
    bruce lee

    i use flash.nvim for this, you enter treesitter mode S and then ; to expand selection

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Thats one of the popular solutions, yeah. Plenty of people prefer that. I tried flash before but this workflow is not for me.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • B
    Bruce lee (again)

    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.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      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.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • N
    Name

    Replacing a whole plugin with this snippet, tysm Pawel

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • S
    Suggon

    Having default keybinds OOTB is great. I knew #36993 was merged but had no idea the PR included keybinds too, thanks!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!