feat(treesitter): incremental selection

Co-authored-by: György Andorka <gyorgy.andorka@protonmail.com>
This commit is contained in:
altermo
2025-12-16 13:03:20 +01:00
committed by Christian Clason
parent e8e694d837
commit 72d3a57f27
7 changed files with 975 additions and 12 deletions

View File

@@ -75,7 +75,7 @@ listed below, if (1) the language server supports the functionality and (2)
the options are empty or were set by the builtin runtime (ftplugin) files. The
options are not restored when the LSP client is stopped or detached.
GLOBAL DEFAULTS *gra* *gri* *grn* *grr* *grt* *i_CTRL-S* *v_an* *v_in*
GLOBAL DEFAULTS *gra* *gri* *grn* *grr* *grt* *i_CTRL-S*
These GLOBAL keymaps are created unconditionally when Nvim starts:
- "gra" (Normal and Visual mode) is mapped to |vim.lsp.buf.code_action()|
@@ -85,8 +85,8 @@ These GLOBAL keymaps are created unconditionally when Nvim starts:
- "grt" is mapped to |vim.lsp.buf.type_definition()|
- "gO" is mapped to |vim.lsp.buf.document_symbol()|
- CTRL-S (Insert mode) is mapped to |vim.lsp.buf.signature_help()|
- "an" and "in" (Visual and Operator-pending mode) are mapped to outer and inner incremental
selections, respectively, using |vim.lsp.buf.selection_range()|
- |v_an| and |v_in| fall back to |vim.lsp.buf.selection_range()| when
buffer has no treesitter parser
BUFFER-LOCAL DEFAULTS

View File

@@ -408,6 +408,7 @@ TREESITTER
• |Query:iter_captures()| supports specifying starting and ending columns.
• |:EditQuery| command gained tab-completion, works with injected languages.
• |LanguageTree:parse()| now accepts a list of ranges.
• |v_an| |v_in| |v_]n| |v_[n| incremental selection of treesitter nodes.
TUI

View File

@@ -607,6 +607,27 @@ Injection queries are currently run over the entire buffer, which can be slow
for large buffers. To disable injections for, e.g., `c`, just place an
empty `queries/c/injections.scm` file in your 'runtimepath'.
==============================================================================
DEFAULTS *treesitter-defaults*
*v_an*
v_an Selects [count]th parent node.
If buffer has no treesitter parser,
falls back to |vim.lsp.buf.selection_range()|.
*v_in*
v_in Selects [count]th previous (or first) child node.
If buffer has no treesitter parser,
falls back to |vim.lsp.buf.selection_range()|.
*v_]n*
v_]n Selects [count]th next node.
*v_[n*
v_[n Selects [count]th previous node.
==============================================================================
VIM.TREESITTER *lua-treesitter*

View File

@@ -164,10 +164,12 @@ you never want any default mappings, call |:mapclear| early in your config.
- |[<Space>| |]<Space>|
- LSP defaults |lsp-defaults|
- K |K-lsp-default|
- |v_an| |v_in|
- gr prefix |gr-default|
- |gra| |gri| |grn| |grr| |grt|
- <C-S> |i_CTRL-S|
- Treesitter defaults |treesitter-defaults|
- |v_an| |v_in|
- |v_]n| |v_[n|
DEFAULT AUTOCOMMANDS
*default-autocmds*

View File

@@ -221,14 +221,6 @@ do
vim.lsp.buf.type_definition()
end, { desc = 'vim.lsp.buf.type_definition()' })
vim.keymap.set({ 'x', 'o' }, 'an', function()
vim.lsp.buf.selection_range(vim.v.count1)
end, { desc = 'vim.lsp.buf.selection_range(vim.v.count1)' })
vim.keymap.set({ 'x', 'o' }, 'in', function()
vim.lsp.buf.selection_range(-vim.v.count1)
end, { desc = 'vim.lsp.buf.selection_range(-vim.v.count1)' })
vim.keymap.set('n', 'gO', function()
vim.lsp.buf.document_symbol()
end, { desc = 'vim.lsp.buf.document_symbol()' })
@@ -452,6 +444,33 @@ do
return 'g@l'
end, { expr = true, desc = 'Add empty line below cursor' })
end
--- incremental treesitter selection mappings (+ lsp fallback)
do
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' })
end
end
--- Default menus

View File

@@ -0,0 +1,583 @@
local Range = require('vim.treesitter._range')
--- This is (currently only) used for saving what child one is in when doing
--- `select_parent` so that if they later `select_child` on the parent-node,
--- they get back to the child-node they were in instead of the parents first
--- child-node.
---
--- @type {[integer]:vim.treesitter.select.node,[any]:any}
local history = {
--- @type integer?
bufnr = nil,
--- @type integer?
changedtick = nil,
--- @type string?
current_node_id = nil,
}
--- The reason for a wrapper around `TSNode` is because we need to store the
--- information about which tstree-range they are in (as a tstree may be
--- disjointed), where region is the return value of
--- `TSTree:included_ranges(false)` with next to eachother ranges combined
--- (e.g. {{0,0,1,1},{1,1,2,2}} -> {{0,0,2,2}}).
---
--- @class vim.treesitter.select.node
--- @field node TSNode
--- @field top vim.treesitter.select.node.top
--- @class vim.treesitter.select.node.top: vim.treesitter.select.node
--- @field ltree vim.treesitter.LanguageTree
--- @field region Range4
local M = {}
--- @param node vim.treesitter.select.node
--- @return string
local function node_id(node)
return ('%s:%s'):format(table.concat({ unpack(node.top.region) }, ':'), node.node:id())
end
--- @param r1 Range4
--- @param r2 Range4
--- @return Range4?
local function range_intersection(r1, r2)
if not Range.intercepts(r1, r2) then
return
end
local rs = Range.cmp_pos.le(r1[1], r1[2], r2[1], r2[2]) and r2 or r1
local re = Range.cmp_pos.ge(r1[3], r1[4], r2[3], r2[4]) and r2 or r1
return { rs[1], rs[2], re[3], re[4] }
end
--- @param r1 Range4
--- @param r2 Range4
--- @boolean
local function range_is_same(r1, r2)
local srow_1, scol_1, erow_1, ecol_1 = Range.unpack4(r1)
local srow_2, scol_2, erow_2, ecol_2 = Range.unpack4(r2)
return srow_1 == srow_2 and scol_1 == scol_2 and erow_1 == erow_2 and ecol_1 == ecol_2
end
--- @param node vim.treesitter.select.node
--- @return Range4
local function node_range(node)
local node_range_ = { node.node:range() }
return range_intersection(node.top.region, node_range_) or { 0, 0, 0, 0 }
end
--- @param node1 vim.treesitter.select.node
--- @param node2 vim.treesitter.select.node
--- @return boolean
local function node_is_same_range(node1, node2)
return range_is_same(node_range(node1), node_range(node2))
end
--- @param node vim.treesitter.select.node
--- @return boolean
local function node_is_size_0(node)
local srow, scol, erow, ecol = Range.unpack4(node_range(node))
return srow == erow and scol == ecol
end
--- @param tsnode TSNode
--- @param relative vim.treesitter.select.node
--- @return vim.treesitter.select.node
local function create_node(tsnode, relative)
assert(tsnode:tree():root():equal(relative.top.node))
--- @type vim.treesitter.select.node
return {
node = tsnode,
top = relative.top,
}
end
--- @param tree TSTree
--- @return Range4[]
local function tree_get_ranges(tree)
--- @type Range4[]
local regions = {}
for _, tree_range in ipairs(tree:included_ranges(false)) do
local prev_region = regions[#regions]
if prev_region and prev_region[3] == tree_range[1] and prev_region[4] == tree_range[2] then
regions[#regions] = { prev_region[1], prev_region[2], tree_range[3], tree_range[4] }
else
table.insert(regions, tree_range)
end
end
return regions
end
--- @param tree TSTree
--- @param region Range4
--- @param ltree vim.treesitter.LanguageTree
--- @return vim.treesitter.select.node.top
local function create_top_node(tree, region, ltree)
--- @type vim.treesitter.select.node.top
local self = {
node = tree:root(),
top = {} --[[@as any]],
ltree = ltree,
region = region,
}
self.top = self
return self
end
--- @param node1 vim.treesitter.select.node.top
--- @param node2 vim.treesitter.select.node.top
--- @return boolean
local function top_node_is_higher_priority(node1, node2)
local srow1, scol1, erow1, ecol1 = Range.unpack4(node_range(node1))
local srow2, scol2, erow2, ecol2 = Range.unpack4(node_range(node2))
if M.TEST_SWITCH_PRIORITY then
if Range.cmp_pos.ne(srow1, scol1, srow2, scol2) then
return Range.cmp_pos.lt(srow1, scol1, srow2, scol2)
elseif Range.cmp_pos.ne(erow1, ecol1, erow2, ecol2) then
return Range.cmp_pos.lt(erow1, ecol1, erow2, ecol2)
elseif node1.ltree:lang() ~= node2.ltree:lang() then
return node1.ltree:lang() > node2.ltree:lang()
end
return node1.node:id() > node2.node:id()
else
if Range.cmp_pos.ne(srow1, scol1, srow2, scol2) then
return Range.cmp_pos.gt(srow1, scol1, srow2, scol2)
elseif Range.cmp_pos.ne(erow1, ecol1, erow2, ecol2) then
return Range.cmp_pos.gt(erow1, ecol1, erow2, ecol2)
elseif node1.ltree:lang() ~= node2.ltree:lang() then
return node1.ltree:lang() < node2.ltree:lang()
end
return node1.node:id() < node2.node:id()
end
end
--- @param range Range4
--- @param top_node vim.treesitter.select.node.top?
--- @param parent_chain vim.treesitter.select.node[]?
--- @return vim.treesitter.select.node|false|nil nil: no parser, false: outside of root-node
--- @return vim.treesitter.select.node[] either `parent_chain` or `alternative_nodes`
local function get_node(range, top_node, parent_chain)
parent_chain = parent_chain or {}
if not top_node then
local parser = vim.treesitter.get_parser(nil, nil, { error = false })
if not parser then
return nil, {}
end
local tree = assert(parser:parse(range))[1]
top_node = create_top_node(tree, assert(tree:included_ranges(false)[1]), parser)
if not Range.contains(node_range(top_node), range) then
return false, { top_node } --[[alternative_nodes]]
end
end
assert(Range.contains(node_range(top_node), range))
--- @param node vim.treesitter.select.node|vim.treesitter.select.node.top
--- @return vim.treesitter.select.node|vim.treesitter.select.node.top
local function node_ignore_overlapped_handle_injection(node)
for _, child in pairs(top_node.ltree:children()) do
for _, child_tree in ipairs(child:trees()) do
for _, child_region in ipairs(tree_get_ranges(child_tree)) do
local child_root_node_range = { child_tree:root():range() }
local child_range = range_intersection(child_region, child_root_node_range)
local child_top_node = create_top_node(child_tree, child_region, child)
if
child_range
and Range.contains(child_range, range)
and (
not node.ltree
or top_node_is_higher_priority(
node --[[@as vim.treesitter.select.node.top]],
child_top_node
)
)
then
return node_ignore_overlapped_handle_injection(child_top_node)
elseif child_range and Range.intercepts(node_range(node), child_range) then
local child_parent_tsnode =
assert(top_node.node:named_descendant_for_range(unpack(child_range)))
if
(not node.ltree and vim.treesitter.is_ancestor(child_parent_tsnode, node.node))
or (
node.ltree
and top_node_is_higher_priority(
node --[[@as vim.treesitter.select.node.top]],
child_top_node
)
)
then
return create_node(child_parent_tsnode, top_node)
end
end
end
end
end
return node
end
local tsnode = assert(top_node.node:named_descendant_for_range(unpack(range)))
local node = create_node(tsnode, top_node)
node = node_ignore_overlapped_handle_injection(node)
if node.ltree then
local root_node_range = { node.node:range() }
local tree_range = node.top.region
local actual_range = assert(range_intersection(tree_range, root_node_range))
local parent_tsnode = assert(top_node.node:named_descendant_for_range(unpack(actual_range)))
table.insert(parent_chain, create_node(parent_tsnode, top_node))
--- @cast node vim.treesitter.select.node.top
return get_node(range, node, parent_chain), parent_chain
end
--- @cast node vim.treesitter.select.node
return node, parent_chain
end
--- @param node vim.treesitter.select.node
--- @param parent_chain vim.treesitter.select.node[]
--- @nodiscard
--- @return vim.treesitter.select.node?
--- @return vim.treesitter.select.node.top?
local function node_get_parent_no_normalize(node, parent_chain)
local parent = node.node:parent()
if parent then
return create_node(parent, node)
end
return table.remove(parent_chain)
end
--- @param node vim.treesitter.select.node
--- @return vim.treesitter.select.node
local function node_normalize_up(node, parent_chain)
while true do
local parent = node_get_parent_no_normalize(node, parent_chain)
if parent and node_is_same_range(parent, node) then
node = parent
else
table.insert(parent_chain, parent)
return node
end
end
--- @diagnostic disable-next-line: missing-return
end
--- @param nodes vim.treesitter.select.node[]
--- @param node vim.treesitter.select.node.top
local function insert_remove_overlapped(nodes, node)
local n = 1
while nodes[n] do
if Range.intercepts(node_range(nodes[n]), node_range(node)) then
if
not nodes
[n] --[[@as any]]
.ltree
or top_node_is_higher_priority(nodes[n] --[[@as vim.treesitter.select.node.top]], node)
then
table.remove(nodes, n)
else
return
end
else
local nrow, ncol, _, _ = Range.unpack4(node_range(nodes[n]))
local _, _, erow, ecol = Range.unpack4(node_range(node))
if Range.cmp_pos.le(erow, ecol, nrow, ncol) then
table.insert(nodes, n, node)
return
end
n = n + 1
end
end
table.insert(nodes, node)
end
--- @param node vim.treesitter.select.node
--- @return vim.treesitter.select.node[]
local function node_get_children_no_normalize(node)
--- @param child_ TSNode
--- @return vim.treesitter.select.node
local children = vim.tbl_map(function(child_)
return create_node(child_, node)
end, node.node:named_children())
node.top.ltree:parse(node_range(node))
for _, child in pairs(node.top.ltree:children()) do
for _, child_tree in ipairs(child:trees()) do
for _, child_region in ipairs(tree_get_ranges(child_tree)) do
local child_root_node_range = { child_tree:root():range() }
local child_range = range_intersection(child_region, child_root_node_range)
if child_range and Range.contains(node_range(node), child_range) then
local child_parent_tsnode =
assert(node.top.node:named_descendant_for_range(unpack(child_range)))
if node.node:equal(child_parent_tsnode) then
local child_node = create_top_node(child_tree, child_region, child)
insert_remove_overlapped(children, child_node)
end
end
end
end
end
return children
end
--- @param range Range4
--- @param node vim.treesitter.select.node
--- @return vim.treesitter.select.node?
local function get_node_contained_in_range(range, node)
for _, child in ipairs(node_get_children_no_normalize(node)) do
if Range.contains(range, node_range(child)) and not node_is_size_0(child) then
return child
elseif Range.intercepts(range, node_range(child)) and not node_is_size_0(child) then
local smallest_node = get_node_contained_in_range(range, child)
if smallest_node then
return smallest_node
end
end
end
end
--- @param node vim.treesitter.select.node
--- @return vim.treesitter.select.node
local function node_normalize_down(node)
for _, child in ipairs(node_get_children_no_normalize(node)) do
if node_is_same_range(node, child) then
return node_normalize_down(child)
end
end
return node
end
local function visual_select(range)
assert(type(range) == 'table')
local srow, scol, erow, ecol = Range.unpack4(range)
local cursor_other_end_of_visual = false
if vim.fn.mode() == 'v' then
local vcol, vrow = vim.fn.col('v'), vim.fn.line('v')
local ccol, cline = vim.fn.col('.'), vim.fn.line('.')
if vrow > cline or (vrow == cline and vcol > ccol) then
cursor_other_end_of_visual = true
end
end
vim.api.nvim_win_set_cursor(0, { srow + 1, scol })
vim.api.nvim_feedkeys(vim.keycode('<C-\\><C-n>v'), 'nx', true)
if not pcall(vim.api.nvim_win_set_cursor, 0, { erow + 1, ecol - 1 }) then
vim.api.nvim_win_set_cursor(0, { erow, #vim.fn.getline(erow) })
end
if cursor_other_end_of_visual then
vim.api.nvim_feedkeys('o', 'nx', true)
end
end
--- @return Range4
local function get_selection()
local pos1 = vim.fn.getpos('v')
local pos2 = vim.fn.getpos('.')
if pos1[2] > pos2[2] or (pos1[2] == pos2[2] and pos1[3] > pos2[3]) then
--- @type Range4,Range4
pos1, pos2 = pos2, pos1
end
local range = { pos1[2] - 1, pos1[3] - 1, pos2[2] - 1, pos2[3] }
if range[4] == #vim.fn.getline(range[3] + 1) + 1 then
range[3] = range[3] + 1
range[4] = 0
end
return range
end
local function get_parent_from_range(range)
local node, parent_chain = get_node(range)
if node == false then
return (assert(parent_chain[1]))
end
if not node then
return
end
if not range_is_same(range, node_range(node)) then
return node
end
node = node_normalize_up(node, parent_chain)
local parent = node_get_parent_no_normalize(node, parent_chain)
if parent then
if
history.bufnr ~= vim.api.nvim_get_current_buf()
or history.changedtick ~= vim.b.changedtick
or history.current_node_id ~= node_id(node)
then
history = {
bufnr = vim.api.nvim_get_current_buf(),
changedtick = vim.b.changedtick,
}
end
table.insert(history, node)
history.current_node_id = node_id(parent)
return parent
end
end
local function get_child_from_range(range)
local node, alternative_child_nodes = get_node(range)
if node == false then
return (assert(alternative_child_nodes[1]))
end
if not node then
return
end
node = node_normalize_down(node)
if not range_is_same(range, node_range(node)) then
history = {}
local smallest_node = get_node_contained_in_range(range, node)
if smallest_node then
return smallest_node
end
return node
end
if
history.bufnr == vim.api.nvim_get_current_buf()
and history.changedtick == vim.b.changedtick
and history.current_node_id == node_id(node)
then
--- @type vim.treesitter.select.node
local child = table.remove(history)
if child then
history.current_node_id = node_id(child)
return child
end
end
history = {}
for _, child in ipairs(node_get_children_no_normalize(node)) do
if not node_is_size_0(child) then
return child
end
end
return node
end
--- @param prev boolean
local function get_sibling_from_range(range, prev)
local node, parent_chain = get_node(range)
if not node then
return
end
node = node_normalize_up(node, parent_chain)
local parent = node_get_parent_no_normalize(node, parent_chain)
if not parent then
return
end
local siblings = node_get_children_no_normalize(parent)
--- @type integer?
local idx
for n, child in ipairs(siblings) do
if node_id(child) == node_id(node) then
idx = n + (prev and -1 or 1)
break
end
end
assert(idx)
while siblings[idx] and node_is_size_0(siblings[idx]) do
idx = idx + (prev and -1 or 1)
end
if siblings[idx] then
return siblings[idx]
end
end
local function get_next_from_range(range)
return get_sibling_from_range(range, false)
end
local function get_prev_from_range(range)
return get_sibling_from_range(range, true)
end
--- @param count integer
--- @param fn fun(range: Range4): vim.treesitter.select.node
local function repeate_apply_range(count, fn)
local range = get_selection()
for _ = 1, count or 1 do
local node = fn(range)
if not node then
break
end
range = node_range(node)
end
if range and count ~= 0 then
visual_select(range)
end
end
--- @param count integer
function M.select_parent(count)
repeate_apply_range(count, get_parent_from_range)
end
--- @param count integer
function M.select_child(count)
repeate_apply_range(count, get_child_from_range)
end
--- @param count integer
function M.select_next(count)
repeate_apply_range(count, get_next_from_range)
end
--- @param count integer
function M.select_prev(count)
repeate_apply_range(count, get_prev_from_range)
end
return M

View File

@@ -0,0 +1,337 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local api = n.api
local fn = n.fn
local clear = n.clear
local eq = t.eq
local exec_lua = n.exec_lua
local feed = n.feed
local function get_selected()
return table.concat(fn.getregion(fn.getpos('v'), fn.getpos('.')), '\n')
end
local function set_lines(lines)
if type(lines) == 'string' then
lines = vim.split(lines, '\n')
end
api.nvim_buf_set_lines(0, 0, -1, true, lines)
end
local function set_filetype(ft)
api.nvim_set_option_value('filetype', ft, { buf = 0 })
end
local function treeselect(cmd_, ...)
if cmd_ == 'select_node' then
cmd_ = 'select_child'
end
exec_lua(function(cmd, ...)
require 'vim.treesitter._select'[cmd](...)
end, cmd_, ...)
end
describe('incremental treesitter selection', function()
before_each(function()
clear()
local code = {
'',
'foo(1)',
'bar(2)',
'',
}
set_lines(code)
set_filetype('lua')
feed('G')
end)
it('works', function()
treeselect('select_node')
eq('foo(1)\nbar(2)\n', get_selected())
treeselect('select_child')
eq('foo(1)', get_selected())
treeselect('select_next')
eq('bar(2)', get_selected())
treeselect('select_prev')
eq('foo(1)', get_selected())
treeselect('select_parent')
eq('foo(1)\nbar(2)\n', get_selected())
end)
it('repeate works', function()
set_lines('foo(1,2,3,4)')
treeselect('select_node')
eq('foo', get_selected())
treeselect('select_next')
eq('(1,2,3,4)', get_selected())
treeselect('select_parent')
eq('foo(1,2,3,4)', get_selected())
treeselect('select_child', 2)
eq('1', get_selected())
treeselect('select_next', 3)
eq('4', get_selected())
treeselect('select_prev', 2)
eq('2', get_selected())
treeselect('select_parent', 2)
eq('foo(1,2,3,4)', get_selected())
treeselect('select_child', 2)
eq('2', get_selected())
end)
it('has history', function()
treeselect('select_node')
treeselect('select_child')
treeselect('select_next')
eq('bar(2)', get_selected())
treeselect('select_parent')
eq('foo(1)\nbar(2)\n', get_selected())
treeselect('select_child')
eq('bar(2)', get_selected())
treeselect('select_prev')
eq('foo(1)', get_selected())
treeselect('select_parent')
eq('foo(1)\nbar(2)\n', get_selected())
treeselect('select_child')
eq('foo(1)', get_selected())
end)
it('correctly selects node as parent when node half selected', function()
feed('kkl', 'v', 'l')
eq('oo', get_selected())
treeselect('select_parent')
eq('foo', get_selected())
end)
it('correctly selects node as child when node half selected', function()
feed('kkl', 'v', 'l')
eq('oo', get_selected())
treeselect('select_child')
eq('foo', get_selected())
end)
it('correctly find child node when node half selected', function()
feed('kkl', 'v', 'j')
eq('oo(1)\nba', get_selected())
treeselect('select_child')
eq('(1)', get_selected())
end)
it('maintainse cursor selection-end-pos', function()
feed('kk')
treeselect('select_node')
eq('foo', get_selected())
treeselect('select_parent')
feed('h')
eq('foo(1', get_selected())
treeselect('select_child')
eq('foo', get_selected())
feed('o')
treeselect('select_parent')
feed('l')
eq('oo(1)', get_selected())
end)
it('handles outside root node', function()
feed('gg', 'v')
eq('', get_selected())
treeselect('select_node')
eq('foo(1)\nbar(2)\n', get_selected())
feed('<esc>gg', 'v')
eq('', get_selected())
treeselect('select_child')
eq('foo(1)\nbar(2)\n', get_selected())
feed('<esc>gg', 'v')
eq('', get_selected())
treeselect('select_parent')
eq('foo(1)\nbar(2)\n', get_selected())
end)
end)
describe('incremental treesitter selection with injections', function()
before_each(function()
clear({ args_rm = { '--cmd' }, args = { '--clean', '--cmd', n.runtime_set } })
end)
it('works', function()
set_lines('```lua\ndo foo() end\n```')
set_filetype('markdown')
feed('gg0')
treeselect('select_node')
treeselect('select_parent')
eq('```lua\ndo foo() end\n```', get_selected())
treeselect('select_child')
treeselect('select_next')
treeselect('select_next')
treeselect('select_child')
treeselect('select_child')
treeselect('select_child')
eq('foo', get_selected())
treeselect('select_parent')
treeselect('select_parent')
treeselect('select_parent')
treeselect('select_prev')
eq('lua', get_selected())
treeselect('select_next')
treeselect('select_next')
eq('```', get_selected())
end)
it('ignores overlapping nodes', function()
do
-- Check that, if injections are disabled, there are nodes overlapping the injection
exec_lua(function()
vim.treesitter.query.set('vimdoc', 'injections', '')
vim.cmd.enew()
end)
set_filetype('help')
set_lines('>lua\n \n foo(\n )')
feed('G0')
treeselect('select_node')
eq(' )', get_selected())
treeselect('select_prev')
eq(' foo(', get_selected())
exec_lua(function()
vim.treesitter.query.set('vimdoc', 'injections', ';; extends')
vim.cmd.enew()
end)
end
set_filetype('help')
set_lines('>lua\n \n foo(\n )')
feed('G0')
treeselect('select_node')
eq('(\n )', get_selected())
treeselect('select_parent')
treeselect('select_parent')
eq('foo(\n )', get_selected())
-- There will be one out of the siblings that wont be covered:
treeselect('select_prev')
eq(' ', get_selected())
end)
it('ignores overlapping injections', function()
exec_lua(function()
vim.treesitter.query.set(
'lua',
'injections',
[[
(comment
content: (_) @injection.content
(#set! injection.language "vim")
(#offset! @injection.content 0 1 0 -3))
(comment
content: (_) @injection.content
(#set! injection.language "c")
(#offset! @injection.content 0 2 0 0))
]]
)
vim.cmd.enew()
end)
-- What the above query does is create the injections as follows (v=vim, c=c):
-- vvvv
-- cccccc
-- -- edit();
set_filetype('lua')
set_lines({ '-- edit();' })
feed('gg0lll')
treeselect('select_node')
if get_selected() == 'edit' then
-- It is random which injection gets higher priority,
-- as the priority uses the treesitter-node's id as a priority
-- So reverse the priority if not favorable
exec_lua("require'vim.treesitter._select'.TEST_SWITCH_PRIORITY=true")
end
feed('<esc>gg0lll')
treeselect('select_node')
eq(' edit();', get_selected())
treeselect('select_child')
eq('dit();', get_selected())
treeselect('select_prev') -- should do nothing
eq('dit();', get_selected())
exec_lua(
"require'vim.treesitter._select'.TEST_SWITCH_PRIORITY=not require'vim.treesitter._select'.TEST_SWITCH_PRIORITY"
)
feed('<esc>gg0lll')
treeselect('select_node')
eq('edit', get_selected())
treeselect('select_next') -- should do nothing
eq('edit', get_selected())
end)
it('handles disjointed trees', function()
exec_lua(function()
vim.treesitter.query.set(
'lua',
'injections',
[[
(comment
content: (_) @injection.content
(#set! injection.language "c")
(#set! injection.combined))
]]
)
vim.cmd.enew()
end)
set_filetype('lua')
set_lines({ '--int foo={', '--1};' })
feed('gg$')
treeselect('select_node')
eq('{', get_selected())
treeselect('select_parent')
treeselect('select_parent')
treeselect('select_parent')
eq('--int foo={', get_selected())
treeselect('select_next')
eq('--1};', get_selected())
treeselect('select_child')
treeselect('select_child')
eq('1}', get_selected())
treeselect('select_prev') -- should do nothing
eq('1}', get_selected())
end)
end)