diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index ff3bd8440b..c004720157 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 6467c0f2e6..0398086e4e 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 58303db639..0dbc8d2da9 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -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* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index b0ab7bfb2e..a6b5b653e3 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -164,10 +164,12 @@ you never want any default mappings, call |:mapclear| early in your config. - |[| |]| - LSP defaults |lsp-defaults| - K |K-lsp-default| - - |v_an| |v_in| - gr prefix |gr-default| - |gra| |gri| |grn| |grr| |grt| - |i_CTRL-S| +- Treesitter defaults |treesitter-defaults| + - |v_an| |v_in| + - |v_]n| |v_[n| DEFAULT AUTOCOMMANDS *default-autocmds* diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua index 833c3a5f6b..e923e8934c 100644 --- a/runtime/lua/vim/_core/defaults.lua +++ b/runtime/lua/vim/_core/defaults.lua @@ -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 diff --git a/runtime/lua/vim/treesitter/_select.lua b/runtime/lua/vim/treesitter/_select.lua new file mode 100644 index 0000000000..3800524c94 --- /dev/null +++ b/runtime/lua/vim/treesitter/_select.lua @@ -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('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 diff --git a/test/functional/treesitter/select_spec.lua b/test/functional/treesitter/select_spec.lua new file mode 100644 index 0000000000..eb1acff85e --- /dev/null +++ b/test/functional/treesitter/select_spec.lua @@ -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('gg', 'v') + eq('', get_selected()) + + treeselect('select_child') + eq('foo(1)\nbar(2)\n', get_selected()) + + feed('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('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('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)