Tree-sitter aware moves for neovim.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-15 15:57:45 +02:00
lua/tree-spider 🌐 Switch off debug log. 2026-08-15 09:02:18 +02:00
plugin 🌱 Import structure from bufmarks.nvim. 2026-07-26 21:06:18 +02:00
.luarc.json 🌱 Import structure from bufmarks.nvim. 2026-07-26 21:06:18 +02:00
LICENSE 🌱 Import structure from bufmarks.nvim. 2026-07-26 21:06:18 +02:00
README.md 📖 Forgot to mention flash.nvim. 2026-08-15 15:57:45 +02:00
stylua.toml Spider moves. 2026-08-09 12:26:24 +02:00

Tree-spider.nvim

Intuitive moves to navigate within the arborescent tree-sitter representation of your file instead of the raw lexical tokens it contains. Try it out!

tree-spider-demo.gif

Minimal starting config:

vim.pack.add { "https://codeberg.org/iago-lito/tree-spider.nvim" }

local ts = require("tree-spider")
ts.setup()

-- Try your first move.
vim.keymap.set("n", "tg", ts.move(ts.parent, ts.Start))

-- Try your first webshoot.
vim.keymap.set("n", "tc", ts.select)

Moves:

  • parent: jump to parent node.
  • child: jump into first/next child.
  • next_leaf: jump to next node recursively, visiting every leaf forward.
  • prev_leaf: jump to previous node recursively, visiting every leaf backwards.
  • next_sibling: jump to next node with the same level, not going deeper.
  • prev_sibling: jump to previous node with the same level, not going deeper.

Every move is parametrized by:

  • Start or End: which side of the node to land on.
  • named or anonymous: whether to consider anonymous nodes.

For instance, ts.move(ts.prev_leaf, ts.End, ts.anonymous), should have you scroll every token end backwards.

Selection

Tree-spider also provides a node selection mode with select, inspired from vim-easymotion, leap.nvim, hop.nvim and flash.nvim.

You can tune the node filtering sequence to your taste by providing a list of filters to select_with instead. There are two possible filters that you may chain or reorder as you like:

  • content: tree-spider highlights the first or last few chars of every node: you type them to narrow down the node you want.
  • position: tree-spider marks nodes instead with combinations of your chosen chars: you type them to narrow down the node you want.

Every filter is also parametrized by:

  • Start: content is a prefix and targets stand on nodes start (if visible).
  • End: content is a suffix and targets stand on nodes end (if visible).

Example configs

For instance, the following select_with is equivalent to the default select:

vim.keymap.set("n", "tc", ts.select_with{
    { ts.content, D.Start }, -- Start by typing target node prefix.
    { ts.position, D.End },  -- Then obtain targets combinations to disambiguate node end.
})

Regarding moves, combinations are yours to set up. Pick your best spammed keys:

-- VERTICAL moves: up and down the tree.
vim.keymap.set("n", "tg", ts.move(ts.parent, ts.Start))
vim.keymap.set("n", "tv", ts.move(ts.child, ts.Start))

-- HORIZONTAL moves: seek same-level nodes.
vim.keymap.set("n", "ts", ts.move(ts.next_sibling, ts.Start))
vim.keymap.set("n", "td", ts.move(ts.previous_sibling, ts.Start))

-- (every move also has an 'End' variant)
vim.keymap.set("n", "tG", ts.move(ts.next_parent, ts.End))
vim.keymap.set("n", "tD", ts.move(ts.previous_sibling, ts.End))

-- Complete BOTTOM moves: visit all leaves forward and backwards.
vim.keymap.set("n", "tn", ts.move(ts.next_leaf, ts.Start))
vim.keymap.set("n", "tN", ts.move(ts.next_leaf, ts.End))
vim.keymap.set("n", "th", ts.move(ts.prev_leaf, ts.Start))
vim.keymap.set("n", "tH", ts.move(ts.prev_leaf, ts.End))

-- ANONYMOUS variants also visit literal leaves.
vim.keymap.set("n", "<m-t>v", ts.move(ts.child, ts.End, ts.anonymous))
vim.keymap.set("n", "<m-t>n", ts.move(ts.next_leaf, ts.Start, ts.anonymous))

Colors

Configure colors with the following highlight groups:

  • TreeSpiderConkNext: color for the first content key to be pressed.
  • TreeSpiderConkPeek: color for upcoming content keys to be pressed.
  • TreeSpiderPoskNext: color for the first position key to be pressed.
  • TreeSpiderPoskPeek: color for upcoming position keys to be pressed.

Options

Here are all default options. Configure to your liking:

ts.setup{

  -- SELECT:
  -- Position chars to be combined for disambiguating nodes.
  positions = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
  -- Number of content keys to type.
  content = 2,
  -- Transform content keys prior to matching against your actual keystroke.
  transforms = { string.lower }, -- (<-  this makes your stroke case-insensitive)
  -- Accept that your keystroke is *almost* the right symbol.
  aliases = {
    ["("] = "()", -- '(' matches either paren.
    ["("] = "()", -- etc.
    ["["] = "[]",
    ["{"] = "{}",
    ["<"] = "<>",
    ["-"] = "-_",
    ["'"] = [['"]],
    ["."] = { pattern = "%p" }, -- (lua pattern)
    [" "] = { pattern = "%s" },
  },
  -- Default behaviour of `ts.select`.
  default_filters = {
    { content, D.Start },
    { position, D.End },
  },

  -- MOVES:
  -- Skip over these particular node types, as if they did not exist.
  skip_nodes = {
  { "comment" }, -- List types to skip for any language.
  {},            -- Map types to skip per-language (e.g. { lua: { "string" } }).
  },
  -- The `next_sibling` move starts by climbing up the tree
  -- to reach the outermost node with the same start as the innermost node.
  -- Sometimes this is not what you expect.
  -- Stop the process earlier by providing nodes types *not* to reach then.
  next_sibling_init_ceiling = {
    lua = "block",
  }
}

Quirks?

If the tree is missing, remember that you can add a parser for almost any language using :TSInstall .

If you are not sure how the moves behave, remember that you can check the tree by yourself any time using :InspectTree. Tree-spider always start from the innermost node where your cursor is.

The select method is merely a heuristic to help you quickly narrow down the node you want among visible nodes. This is not easy because there can be many nodes, most of which share the same starting and/or ending positions, resulting in targets possibly overlapping and ambiguity remaining. Try configuring select_with until you find the best heuristic in your case.

There is no way to "jump" to a distant node without selecting it. If you need to do that, remember that vim-easymotion, leap.nvim, hop.nvim and flash.nvim (at least) are exactly designed for this purpose. They are not restricted to nodes starts and ends positions, and they even work when treesitter is not available. Check them out! Tree-spider moves are meant to be local instead, and to also work when you have no precise idea which node you want to go next.

See also