From 3342a54548cd2a9ab3536c479c4989a1aea7b327 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 28 Dec 2023 16:33:55 +0100 Subject: [PATCH 01/69] implement optional argnodes. In that case, there will be a `nil` at the place of the text of the node in the arg-list. They have to be explicitly marked as optional, since we currently have the behaviour of not updating nodes at all if an argnode is missing. --- lua/luasnip/nodes/functionNode.lua | 7 ++++++ lua/luasnip/nodes/node.lua | 34 +++++++++++++++++++----------- lua/luasnip/nodes/optional_arg.lua | 12 +++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 lua/luasnip/nodes/optional_arg.lua diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index cb1a77ae7..f1812fe17 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -6,6 +6,7 @@ local types = require("luasnip.util.types") local tNode = require("luasnip.nodes.textNode").textNode local extend_decorator = require("luasnip.util.extend_decorator") local key_indexer = require("luasnip.nodes.key_indexer") +local opt_args = require("luasnip.nodes.optional_arg") local function F(fn, args, opts) opts = opts or {} @@ -56,6 +57,9 @@ function FunctionNode:update() -- don't expand tabs in parent.indentstr, use it as-is. self:set_text(util.indent(text, self.parent.indentstr)) + + -- assume that functionNode can't have a parent as its dependent, there is + -- no use for that I think. self:update_dependents() end @@ -122,6 +126,9 @@ function FunctionNode:set_dependents() append_list[#append_list + 1] = "dependent" for _, arg in ipairs(self.args_absolute) do + if opt_args.is_opt(arg) then + arg = arg.ref + end -- if arg is a luasnip-node, just insert it as the key. -- important!! rawget, because indexing absolute_indexer with some key -- appends the key. diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index b3cce555e..c47c2b054 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -5,6 +5,7 @@ local ext_util = require("luasnip.util.ext_opts") local events = require("luasnip.util.events") local key_indexer = require("luasnip.nodes.key_indexer") local types = require("luasnip.util.types") +local opt_args = require("luasnip.nodes.optional_arg") local Node = {} @@ -302,7 +303,12 @@ end local function get_args(node, get_text_func_name) local argnodes_text = {} - for _, arg in ipairs(node.args_absolute) do + for key, arg in ipairs(node.args_absolute) do + local is_optional = opt_args.is_opt(arg) + if is_optional then + arg = arg.ref + end + local argnode if key_indexer.is_key(arg) then argnode = node.parent.snippet.dependents_dict:get({ @@ -327,18 +333,22 @@ local function get_args(node, get_text_func_name) dict_key[#dict_key] = nil end -- maybe the node is part of a dynamicNode and not yet generated. - if not argnode then - return nil - end - - local argnode_text = argnode[get_text_func_name](argnode) - -- can only occur with `get_text`. If one returns nil, the argnode - -- isn't visible or some other error occured. Either way, return nil - -- to signify that not all argnodes are available. - if not argnode_text then - return nil + if argnode then + local argnode_text = argnode[get_text_func_name](argnode) + -- can only occur with `get_text`. If one returns nil, the argnode + -- isn't visible or some other error occured. Either way, return nil + -- to signify that not all argnodes are available. + if not argnode_text then + return nil + end + argnodes_text[key] = argnode_text + else + if is_optional then + argnodes_text[key] = nil + else + return nil + end end - table.insert(argnodes_text, argnode_text) end return argnodes_text diff --git a/lua/luasnip/nodes/optional_arg.lua b/lua/luasnip/nodes/optional_arg.lua new file mode 100644 index 000000000..fd54fa195 --- /dev/null +++ b/lua/luasnip/nodes/optional_arg.lua @@ -0,0 +1,12 @@ +local M = {} + +local opt_mt = {} +function M.new_opt(ref) + return setmetatable({ ref = ref }, opt_mt) +end + +function M.is_opt(t) + return getmetatable(t) == opt_mt +end + +return M From 8f82c73997f75d705950549469b02b08a8b22809 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:26:01 +0100 Subject: [PATCH 02/69] implement subtree_do, invokes callbacks on tree of nodes (in a snippet). --- lua/luasnip/nodes/choiceNode.lua | 6 ++++++ lua/luasnip/nodes/dynamicNode.lua | 14 ++++++++++++++ lua/luasnip/nodes/node.lua | 6 ++++++ lua/luasnip/nodes/restoreNode.lua | 14 ++++++++++++++ lua/luasnip/nodes/snippet.lua | 8 ++++++++ lua/luasnip/nodes/util.lua | 13 +++++++++++++ 6 files changed, 61 insertions(+) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 22de611b2..e0cfa9daf 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -422,6 +422,12 @@ function ChoiceNode:extmarks_valid() return node_util.generic_extmarks_valid(self, self.active_choice) end +function ChoiceNode:subtree_do(opts) + opts.pre(self) + self.active_choice:subtree_do(opts) + opts.post(self) +end + return { C = C, } diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 0daad6f20..aa4a5dbd4 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -434,6 +434,20 @@ function DynamicNode:extmarks_valid() return true end +function DynamicNode:subtree_do(opts) + opts.pre(self) + if opts.static then + if self.static_snip then + self.static_snip:subtree_do(opts) + end + else + if self.snip then + self.snip:subtree_do(opts) + end + end + opts.post(self) +end + return { D = D, } diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index c47c2b054..ce22b64f8 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -647,6 +647,12 @@ function Node:leaf() ) end + +function Node:subtree_do(opts) + opts.pre(self) + opts.post(self) +end + return { Node = Node, focus_node = focus_node, diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index 48c8448af..49d5c6ce5 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -302,6 +302,20 @@ function RestoreNode:extmarks_valid() return node_util.generic_extmarks_valid(self, self.snip) end +function RestoreNode:subtree_do(opts) + opts.pre(self) + if self.snip then + self.snip:subtree_do(opts) + else + if opts.static then + -- try using stored snippet for recursion when static and regular + -- snip does not exist. + self.parent.snippet.stored[self.key]:subtree_do(opts) + end + end + opts.post(self) +end + return { R = R, } diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index bfd39b5f1..db8c3cad4 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1562,6 +1562,14 @@ function Snippet:extmarks_valid() return true end +function Snippet:subtree_do(opts) + opts.pre(self) + for _, child in ipairs(self.nodes) do + child:subtree_do(opts) + end + opts.post(self) +end + return { Snippet = Snippet, S = S, diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 03c9e331a..6d9022286 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -765,6 +765,18 @@ local function nodelist_adjust_rgravs( end end + +local function node_subtree_do(node, opts) + -- provide default-values. + if not opts.pre then + opts.pre = util.nop + end + if not opts.post then + opts.post = util.nop + end + + node:subtree_do(opts) +end return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -787,4 +799,5 @@ return { interactive_node = interactive_node, root_path = root_path, nodelist_adjust_rgravs = nodelist_adjust_rgravs, + node_subtree_do = node_subtree_do } From c61d08365694a414ac4aa5c689e2a1d3f539480e Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:30:34 +0100 Subject: [PATCH 03/69] make resolve_position work for static snippets. dynamicNode has to return .static_snip instead of .snip --- lua/luasnip/nodes/dynamicNode.lua | 8 ++++++-- lua/luasnip/nodes/util.lua | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index aa4a5dbd4..c0d96556e 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -408,9 +408,13 @@ end DynamicNode.make_args_absolute = FunctionNode.make_args_absolute DynamicNode.set_dependents = FunctionNode.set_dependents -function DynamicNode:resolve_position(position) +function DynamicNode:resolve_position(position, static) -- position must be 0, there are no other options. - return self.snip + if static then + return self.static_snip + else + return self.snip + end end function DynamicNode:subtree_set_pos_rgrav(pos, direction, rgrav) diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 6d9022286..5527ee6ce 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -65,7 +65,7 @@ local function wrap_args(args) end -- includes child, does not include parent. -local function get_nodes_between(parent, child) +local function get_nodes_between(parent, child, static) local nodes = {} -- special case for nodes without absolute_position (which is only @@ -81,7 +81,7 @@ local function get_nodes_between(parent, child) local indx = #parent.absolute_position + 1 local prev = parent while child_pos[indx] do - local next = prev:resolve_position(child_pos[indx]) + local next = prev:resolve_position(child_pos[indx], static) nodes[#nodes + 1] = next prev = next indx = indx + 1 @@ -704,12 +704,12 @@ local function snippettree_find_undamaged_node(pos, opts) return prev_parent, prev_parent_children, child_indx, node end -local function root_path(node) +local function root_path(node, static) local path = {} while node do local node_snippet = node.parent.snippet - local snippet_node_path = get_nodes_between(node_snippet, node) + local snippet_node_path = get_nodes_between(node_snippet, node, static) -- get_nodes_between gives parent -> node, but we need -- node -> parent => insert back to front. for i = #snippet_node_path, 1, -1 do From 3f56b386b2fb15e035074482b347d73e2abff505 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:31:35 +0100 Subject: [PATCH 04/69] fix: make root_path work for snippets. --- lua/luasnip/nodes/util.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 5527ee6ce..6e06d1f0f 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -708,7 +708,14 @@ local function root_path(node, static) local path = {} while node do - local node_snippet = node.parent.snippet + local node_snippet + if node.parent == nil then + -- node is snippet. + node_snippet = node + else + node_snippet = node.parent.snippet + end + local snippet_node_path = get_nodes_between(node_snippet, node, static) -- get_nodes_between gives parent -> node, but we need -- node -> parent => insert back to front. From de8dd213d0da5378a8c02a8ae5cc61c83d9e3792 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 10:06:18 +0100 Subject: [PATCH 05/69] implement subtree_leave_entered, for leaving only entered nodes. --- lua/luasnip/nodes/choiceNode.lua | 10 ++++++++++ lua/luasnip/nodes/dynamicNode.lua | 7 +++++++ lua/luasnip/nodes/insertNode.lua | 27 +++++++++++++++++++++++++++ lua/luasnip/nodes/node.lua | 4 ++++ lua/luasnip/nodes/restoreNode.lua | 9 +++++++++ lua/luasnip/nodes/snippet.lua | 17 +++++++++++++++++ 6 files changed, 74 insertions(+) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index e0cfa9daf..5d2e87a01 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -146,6 +146,7 @@ function ChoiceNode:input_enter(_, dry_run) session.active_choice_nodes[vim.api.nvim_get_current_buf()] = self self.visited = true self.active = true + self.input_active = true self:event(events.enter) end @@ -156,6 +157,8 @@ function ChoiceNode:input_leave(_, dry_run) return end + self.input_active = false + self:event(events.leave) self.mark:update_opts(self:get_passive_ext_opts()) @@ -428,6 +431,13 @@ function ChoiceNode:subtree_do(opts) opts.post(self) end +function ChoiceNode:subtree_leave_entered() + if self.input_active then + self.active_choice:subtree_leave_entered() + self:input_leave() + end +end + return { C = C, } diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index c0d96556e..4de7f0ca3 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -452,6 +452,13 @@ function DynamicNode:subtree_do(opts) opts.post(self) end +function DynamicNode:subtree_leave_entered() + if self.active then + self.snip:subtree_leave_entered() + self:input_leave() + end +end + return { D = D, } diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index e4a9f832a..f1ba9742d 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -20,6 +20,8 @@ local function I(pos, static_text, opts) type = types.exitNode, -- will only be needed for 0-node, -1-node isn't set with this. ext_gravities_active = { false, false }, + inner_active = false, + input_active = false }, opts) else return InsertNode:new({ @@ -29,6 +31,7 @@ local function I(pos, static_text, opts) dependents = {}, type = types.insertNode, inner_active = false, + input_active = false }, opts) end end @@ -80,6 +83,8 @@ function ExitNode:input_leave(no_move, dry_run) return end + self.input_active = false + if self.pos == 0 then InsertNode.input_leave(self, no_move, dry_run) else @@ -104,6 +109,7 @@ function InsertNode:input_enter(no_move, dry_run) end self.visited = true + self.input_active = true self.mark:update_opts(self.ext_opts.active) -- no_move only prevents moving the cursor, but the active node should @@ -237,6 +243,7 @@ function InsertNode:input_leave(_, dry_run) return end + self.input_active = false self:event(events.leave) self:update_dependents() @@ -247,10 +254,13 @@ function InsertNode:exit() if self.inner_first then self.inner_first:exit() end + + -- reset runtime-acquired values. self.visible = false self.inner_first = nil self.inner_last = nil self.inner_active = false + self.input_active = false self.mark:clear() end @@ -306,6 +316,23 @@ function InsertNode:subtree_set_rgrav(rgrav) end end +function InsertNode:subtree_leave_entered() + if not self.input_active then + -- is not directly active, and does not contain an active child. + return + else + -- first leave children, if they're active, then self. + if self.inner_active then + local nested_snippets = self:child_snippets() + for _, snippet in ipairs(nested_snippets) do + snippet:subtree_leave_entered() + end + self:input_leave_children() + end + self:input_leave() + end +end + return { I = I, } diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index ce22b64f8..130ebf47d 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -653,6 +653,10 @@ function Node:subtree_do(opts) opts.post(self) end +-- all nodes that can be entered have an override, only need to nop this for +-- those that don't. +function Node:subtree_leave_entered() end + return { Node = Node, focus_node = focus_node, diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index 49d5c6ce5..36dc6609d 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -316,6 +316,15 @@ function RestoreNode:subtree_do(opts) opts.post(self) end +function RestoreNode:subtree_leave_entered() + if self.active then + if self.snip then + self.snip:subtree_leave_entered() + end + self:input_leave() + end +end + return { R = R, } diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index db8c3cad4..725b9c205 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1570,6 +1570,23 @@ function Snippet:subtree_do(opts) opts.post(self) end + +-- affect all children nested into this snippet. +function Snippet:subtree_leave_entered() + if self.active then + for _, node in ipairs(self.nodes) do + node:subtree_leave_entered() + end + self:input_leave() + else + if self.type ~= types.snippetNode then + -- the exit-nodes (-1 and 0) may be active if the snippet itself is + -- not; just do these two calls, no hurt if they're not active. + self.prev:subtree_leave_entered() + self.insert_nodes[0]:subtree_leave_entered() + end + end +end return { Snippet = Snippet, S = S, From 20b61a22c4300f6225e425fb5a7531804220ceed Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 10:30:17 +0100 Subject: [PATCH 06/69] overhaul snippet-updates. Previously: InsertNodes trigger an update on input_leave. This works, but, if updates invalidate nodes that are currently involved in being jumped over, we'd have to abort and roll back the jump, or do some other recovery. To avoid this, update_dependents is done _before_ any jumping is performed (this is really more elegant now since we can keep track of which nodes are "above" some insertNode, and then just get all of them and their dependents (see node_util.find_node_dependents/collect_dependents)) So, now `ls.jump` does essentially: * store position of cursor relative to active node * collect nodes that depend on the text of this node (so, all dependents of all parents of this node) * update them * try to find an equivalent node (node with the same key, or if a restoreNode is present, the exact same node :D) and perform the jump from it * if an equivalent node could not be found, just enter the first dynamicNode (starting from the previously active node) and enter it Obviously, this only really works well if an equivalent of the current node can be found in the new generated nodes. Similarly, active_update_dependents --- lua/luasnip/init.lua | 196 ++++++++++++++++++++--------- lua/luasnip/nodes/choiceNode.lua | 33 +---- lua/luasnip/nodes/dynamicNode.lua | 33 +++-- lua/luasnip/nodes/functionNode.lua | 6 +- lua/luasnip/nodes/insertNode.lua | 5 - lua/luasnip/nodes/node.lua | 121 ++++++++---------- lua/luasnip/nodes/restoreNode.lua | 16 --- lua/luasnip/nodes/snippet.lua | 46 ++----- lua/luasnip/nodes/textNode.lua | 2 - lua/luasnip/nodes/util.lua | 72 +++++++++++ tests/integration/session_spec.lua | 11 +- 11 files changed, 299 insertions(+), 242 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 96cced870..28d230a61 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -134,18 +134,59 @@ local function unlink_current() unlink_set_adjacent_as_current_no_log(current.parent.snippet) end --- return next active node. -local function safe_jump_current(dir, no_move, dry_run) - local node = session.current_nodes[vim.api.nvim_get_current_buf()] - if not node then - return nil +local store_id = 0 +local function store_cursor_node_relative(node) + local data = {} + + local snippet_current_node = node + + -- store for each snippet! + -- the innermost snippet may be destroyed, and we would have to restore the + -- cursor in a snippet above that. + while snippet_current_node do + local snip = snippet_current_node:get_snippet() + + local snip_data = {} + + snip_data.key = node.key + node.store_id = store_id + snip_data.store_id = store_id + snip_data.node = snippet_current_node + + store_id = store_id + 1 + + snip_data.cursor_end_relative = util.pos_sub(util.get_cursor_0ind(), node.mark:get_endpoint(1)) + + data[snip] = snip_data + + snippet_current_node = snip:get_snippet().parent_node end - local ok, res = pcall(node.jump_from, node, dir, no_move, dry_run) - if ok then - return res - else - local snip = node.parent.snippet + return data +end + +local function get_corresponding_node(parent, data) + return parent:find_node(function(test_node) + return (test_node.store_id == data.store_id) or (data.key ~= nil and test_node.key == data.key) + end) +end + +local function restore_cursor_pos_relative(node, data) + util.set_cursor_0ind( + util.pos_add( + node.mark:get_endpoint(1), + data.cursor_end_relative + ) + ) +end + +local function node_update_dependents_preserve_position(node, opts) + local restore_data = store_cursor_node_relative(node) + + -- update all nodes that depend on this one. + local ok, res = pcall(node.update_dependents, node, {own=true, parents=true}) + if not ok then + local snip = node:get_snippet() unlink_set_adjacent_as_current( snip, @@ -153,9 +194,90 @@ local function safe_jump_current(dir, no_move, dry_run) snip.trigger, res ) - return session.current_nodes[vim.api.nvim_get_current_buf()] + return { jump_done = false, new_node = session.current_nodes[vim.api.nvim_get_current_buf()] } + end + + -- update successful => check if the current node is still visible. + if node.visible then + if not opts.no_move and opts.restore_position then + -- node is visible: restore position. + local active_snippet = node:get_snippet() + restore_cursor_pos_relative(node, restore_data[active_snippet]) + end + + return { jump_done = false, new_node = node } + else + -- node not visible => need to find a new node to set as current. + + -- first, find leafmost (starting at node) visible node. + local active_snippet = node:get_snippet() + while not active_snippet.visible do + local parent_node = active_snippet.parent_node + if not parent_node then + -- very unlikely/not possible: all snippets are exited. + return { jump_done = false, new_node = nil } + end + active_snippet = parent_node:get_snippet() + end + + -- have found first visible snippet => look for dynamicNode. + local snip_restore_data = restore_data[active_snippet] + local node_parent = snip_restore_data.node.parent + + -- find visible dynamicNode that contained the (now-inactive) insertNode. + -- since the node was no longer visible after an update, it must have + -- been contained in a dynamicNode, and we don't have to handle the + -- case that we can't find it. + while node_parent.dynamicNode == nil or node_parent.dynamicNode.visible == false do + node_parent = node_parent.parent + end + local d = node_parent.dynamicNode + assert(d.active, "Visible dynamicNode that was a parent of the current node is not active after the update!! If you get this message, please open an issue with LuaSnip!") + + local new_node = get_corresponding_node(d, snip_restore_data) + + if new_node then + node_util.refocus(d, new_node) + + if not opts.no_move and opts.restore_position then + -- node is visible: restore position + restore_cursor_pos_relative(new_node, snip_restore_data) + end + + return { jump_done = false, new_node = new_node } + else + -- could not find corresponding node -> just jump into the + -- dynamicNode that should have generated it. + return { jump_done = true, new_node = d:jump_into_snippet(opts.no_move) } + end + end +end + +-- return next active node. +local function safe_jump_current(dir, no_move, dry_run) + local node = session.current_nodes[vim.api.nvim_get_current_buf()] + if not node then + return nil + end + + -- don't update for -1-node. + if not dry_run and node.pos >= 0 then + local upd_res = node_update_dependents_preserve_position(node, { no_move = no_move, restore_position = false }) + if upd_res.jump_done then + return upd_res.new_node + else + node = upd_res.new_node + end + end + + if node then + local ok, res = pcall(node.jump_from, node, dir, no_move, dry_run) + if ok then + return res + end end end + local function jump(dir) local current = session.current_nodes[vim.api.nvim_get_current_buf()] if current then @@ -484,52 +606,12 @@ end local function active_update_dependents() local active = session.current_nodes[vim.api.nvim_get_current_buf()] - -- special case for startNode, cannot focus on those (and they can't - -- have dependents) - -- don't update if a jump/change_choice is in progress. - if not session.jump_active and active and active.pos > 0 then - -- Save cursor-pos to restore later. - local cur = util.get_cursor_0ind() - local cur_mark = vim.api.nvim_buf_set_extmark( - 0, - session.ns_id, - cur[1], - cur[2], - { right_gravity = false } - ) - - local ok, err = pcall(active.update_dependents, active) - if not ok then - unlink_set_adjacent_as_current( - active.parent.snippet, - "Error while updating dependents for snippet %s due to error %s", - active.parent.snippet.trigger, - err - ) - return - end - - -- 'restore' orientation of extmarks, may have been changed by some set_text or similar. - ok, err = pcall(active.focus, active) - if not ok then - unlink_set_adjacent_as_current( - active.parent.snippet, - "Error while entering node in snippet %s: %s", - active.parent.snippet.trigger, - err - ) - - return - end - - -- Don't account for utf, nvim_win_set_cursor doesn't either. - cur = vim.api.nvim_buf_get_extmark_by_id( - 0, - session.ns_id, - cur_mark, - { details = false } - ) - util.set_cursor_0ind(cur) + -- don't update if a jump/change_choice is in progress, or if we don't have + -- an active node. + if not session.jump_active and active ~= nil then + local upd_res = node_update_dependents_preserve_position(active, { no_move = false, restore_position = true }) + upd_res.new_node:focus() + session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_node end end diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 5d2e87a01..75686f7c1 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -24,18 +24,6 @@ function ChoiceNode:init_nodes() end, }) - -- replace nodes' original update_dependents with function that also - -- calls this choiceNodes' update_dependents. - -- - -- cannot define as `function node:update_dependents()` as _this_ - -- choiceNode would be `self`. - -- Also rely on node.choice, as using `self` there wouldn't be caught - -- by copy and the wrong node would be updated. - choice.update_dependents = function(node) - node:_update_dependents() - node.choice:update_dependents() - end - choice.next_choice = self.choices[i + 1] choice.prev_choice = self.choices[i - 1] end @@ -162,7 +150,7 @@ function ChoiceNode:input_leave(_, dry_run) self:event(events.leave) self.mark:update_opts(self:get_passive_ext_opts()) - self:update_dependents() + session.active_choice_nodes[vim.api.nvim_get_current_buf()] = self.prev_choice_node self.active = false @@ -261,7 +249,7 @@ function ChoiceNode:set_choice(choice, current_node) -- cleared mark in set_mark_rgrav (which will be called in -- self:set_text({""}) a few lines below). self.active_choice = nil - self:set_text({ "" }) + self:set_text_raw({ "" }) self.active_choice = choice @@ -282,8 +270,7 @@ function ChoiceNode:set_choice(choice, current_node) self.active_choice:subtree_set_pos_rgrav(to, -1, true) self.active_choice:update_restore() - self.active_choice:update_all_dependents() - self:update_dependents() + self:update_dependents({own=true, parents=true, children=true}) -- Another node may have been entered in update_dependents. self:focus() @@ -384,20 +371,6 @@ function ChoiceNode:set_argnodes(dict) end end -function ChoiceNode:update_all_dependents() - -- call the version that only updates this node. - self:_update_dependents() - - self.active_choice:update_all_dependents() -end - -function ChoiceNode:update_all_dependents_static() - -- call the version that only updates this node. - self:_update_dependents_static() - - self.active_choice:update_all_dependents_static() -end - function ChoiceNode:resolve_position(position) return self.choices[position] end diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 4de7f0ca3..83c557cb3 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -44,7 +44,6 @@ function DynamicNode:input_leave(_, dry_run) end self:event(events.leave) - self:update_dependents() self.active = false self.mark:update_opts(self:get_passive_ext_opts()) end @@ -112,6 +111,11 @@ function DynamicNode:jump_into(dir, no_move, dry_run) end end +function DynamicNode:jump_into_snippet(no_move) + self.active = false + return self:jump_into(1, no_move, false) +end + function DynamicNode:update() local args = self:get_args() if vim.deep_equal(self.last_args, args) then @@ -137,11 +141,12 @@ function DynamicNode:update() self.snip.old_state, unpack(self.user_args) ) + self.snip:subtree_leave_entered() self.snip:exit() self.snip = nil -- focuses node. - self:set_text({ "" }) + self:set_text_raw({ "" }) else self:focus() if not args then @@ -170,10 +175,6 @@ function DynamicNode:update() tmp.mark = self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) tmp.dynamicNode = self - tmp.update_dependents = function(node) - node:_update_dependents() - node.dynamicNode:update_dependents() - end tmp:init_positions(self.snip_absolute_position) tmp:init_insert_positions(self.snip_absolute_insert_position) @@ -204,9 +205,11 @@ function DynamicNode:update() -- - a node could only depend on nodes outside of tmp -- - a node outside of tmp could depend on one inside of tmp tmp:update() - tmp:update_all_dependents() - - self:update_dependents() + -- update nodes that depend on this dynamicNode, nodes that are parents + -- (and thus have changed text after this update), and all of the + -- children's depedents (since they may have dependents outside this + -- dynamicNode, who have not yet been updated) + self:update_dependents({own=true, children=true, parents=true}) end local update_errorstring = [[ @@ -268,10 +271,6 @@ function DynamicNode:update_static() tmp.snippet = self.parent.snippet tmp.dynamicNode = self - tmp.update_dependents_static = function(node) - node:_update_dependents_static() - node.dynamicNode:update_dependents_static() - end tmp:resolve_child_ext_opts() tmp:resolve_node_ext_opts() @@ -295,13 +294,11 @@ function DynamicNode:update_static() tmp:static_init() - tmp:update_static() - -- updates dependents in tmp. - tmp:update_all_dependents_static() - self.static_snip = tmp + + tmp:update_static() -- updates own dependents. - self:update_dependents_static() + self:update_dependents_static({own=true, parents=true, children=true}) end function DynamicNode:exit() diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index f1812fe17..fdcfa0112 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -56,11 +56,11 @@ function FunctionNode:update() end -- don't expand tabs in parent.indentstr, use it as-is. - self:set_text(util.indent(text, self.parent.indentstr)) + self:set_text_raw(util.indent(text, self.parent.indentstr)) -- assume that functionNode can't have a parent as its dependent, there is -- no use for that I think. - self:update_dependents() + self:update_dependents({own=true, parents=true}) end local update_errorstring = [[ @@ -99,7 +99,7 @@ end function FunctionNode:update_restore() -- only if args still match. if self.static_text and vim.deep_equal(self:get_args(), self.last_args) then - self:set_text(self.static_text) + self:set_text_raw(self.static_text) else self:update() end diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index f1ba9742d..39e9f1563 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -92,13 +92,9 @@ function ExitNode:input_leave(no_move, dry_run) end end -function ExitNode:_update_dependents() end function ExitNode:update_dependents() end -function ExitNode:update_all_dependents() end -function ExitNode:_update_dependents_static() end function ExitNode:update_dependents_static() end -function ExitNode:update_all_dependents_static() end function ExitNode:is_interactive() return true end @@ -246,7 +242,6 @@ function InsertNode:input_leave(_, dry_run) self.input_active = false self:event(events.leave) - self:update_dependents() self.mark:update_opts(self:get_passive_ext_opts()) end diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 130ebf47d..977057ee8 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -181,78 +181,6 @@ end function Node:input_leave_children() end function Node:input_enter_children() end -local function find_dependents(self, position_self, dict) - local nodes = {} - - -- this might also be called from a node which does not possess a position! - -- (for example, a functionNode may be depended upon via its key) - if position_self then - position_self[#position_self + 1] = "dependents" - vim.list_extend(nodes, dict:find_all(position_self, "dependent") or {}) - position_self[#position_self] = nil - end - - vim.list_extend( - nodes, - dict:find_all({ self, "dependents" }, "dependent") or {} - ) - - if self.key then - vim.list_extend( - nodes, - dict:find_all({ "key", self.key, "dependents" }, "dependent") or {} - ) - end - - return nodes -end - -function Node:_update_dependents() - local dependent_nodes = find_dependents( - self, - self.absolute_insert_position, - self.parent.snippet.dependents_dict - ) - if #dependent_nodes == 0 then - return - end - for _, node in ipairs(dependent_nodes) do - if node.visible then - node:update() - end - end -end - --- _update_dependents is the function to update the nodes' dependents, --- update_dependents is what will actually be called. --- This allows overriding update_dependents in a parent-node (eg. snippetNode) --- while still having access to the original function (for subsequent overrides). -Node.update_dependents = Node._update_dependents --- update_all_dependents is used to update all nodes' dependents in a --- snippet-tree. Necessary in eg. set_choice (especially since nodes may have --- dependencies outside the tree itself, so update_all_dependents should take --- care of those too.) -Node.update_all_dependents = Node._update_dependents - -function Node:_update_dependents_static() - local dependent_nodes = find_dependents( - self, - self.absolute_insert_position, - self.parent.snippet.dependents_dict - ) - if #dependent_nodes == 0 then - return - end - for _, node in ipairs(dependent_nodes) do - if node.static_visible then - node:update_static() - end - end -end - -Node.update_dependents_static = Node._update_dependents_static -Node.update_all_dependents_static = Node._update_dependents_static - function Node:update() end function Node:update_static() end @@ -601,6 +529,20 @@ function Node:focus() end function Node:set_text(text) + local text_indented = util.indent(text, self.parent.indentstr) + + if self:get_snippet().___static_expanded then + self.static_text = text_indented + self:update_dependents_static({own=true, parents=true}) + else + if self.visible then + self:set_text_raw(text_indented) + self:update_dependents({own=true, parents=true}) + end + end +end + +function Node:set_text_raw(text) self:focus() local node_from, node_to = self.mark:pos_begin_end_raw() @@ -647,12 +589,47 @@ function Node:leaf() ) end +function Node:parent_of(node) + for i = 1, #self.absolute_position do + if self.absolute_position[i] ~= node.absolute_position[i] then + return false + end + end + + return true +end + +-- self has to be visible/in the buffer. +-- none of the node's ancestors may contain self. +function Node:update_dependents(which) + -- false: don't set static + local dependents = node_util.collect_dependents(self, which, false) + for _, node in ipairs(dependents) do + if node.visible then + node:update() + end + end +end + +function Node:update_dependents_static(which) + -- true: set static + local dependents = node_util.collect_dependents(self, which, true) + for _, node in ipairs(dependents) do + if node.static_visible then + node:update_static() + end + end +end function Node:subtree_do(opts) opts.pre(self) opts.post(self) end +function Node:get_snippet() + return self.parent.snippet +end + -- all nodes that can be entered have an override, only need to nop this for -- those that don't. function Node:subtree_leave_entered() end diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index 36dc6609d..c63be00bd 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -66,7 +66,6 @@ function RestoreNode:input_leave(_, dry_run) self:event(events.leave) - self:update_dependents() self.active = false self.mark:update_opts(self:get_passive_ext_opts()) @@ -102,11 +101,6 @@ function RestoreNode:put_initial(pos) tmp.snippet = self.parent.snippet tmp.restore_node = self - tmp.update_dependents = function(node) - node:_update_dependents() - -- self is restoreNode. - node.restore_node:update_dependents() - end tmp:resolve_child_ext_opts() tmp:resolve_node_ext_opts() @@ -247,16 +241,6 @@ function RestoreNode:insert_to_node_absolute(position) return self.snip and self.snip:insert_to_node_absolute(position) end -function RestoreNode:update_all_dependents() - self:_update_dependents() - self.snip:update_all_dependents() -end - -function RestoreNode:update_all_dependents_static() - self:_update_dependents_static() - self.parent.snippet.stored[self.key]:_update_dependents_static() -end - function RestoreNode:init_insert_positions(position_so_far) Node.init_insert_positions(self, position_so_far) self.snip_absolute_insert_position = diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 725b9c205..c4a60bc32 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -107,11 +107,6 @@ function Snippet:init_nodes() insert_nodes[node.pos] = node end end - - node.update_dependents = function(node) - node:_update_dependents() - node.parent:update_dependents() - end end if insert_nodes[1] then @@ -362,16 +357,6 @@ local function _S(snip, nodes, opts) -- is propagated to all subsnippets, used to quickly find the outer snippet snip.snippet = snip - -- if the snippet is expanded inside another snippet (can be recognized by - -- non-nil parent_node), the node of the snippet this one is inside has to - -- update its dependents. - function snip:_update_dependents() - if self.parent_node then - self.parent_node:update_dependents() - end - end - snip.update_dependents = snip._update_dependents - snip:init_nodes() if not snip.insert_nodes[0] then @@ -661,6 +646,7 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) -- enter current node, it will contain the new snippet. current_node:input_enter_children() end + else -- if no parent_node, completely leave. node_util.refocus(current_node, nil) @@ -770,7 +756,7 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) self.mark = mark(old_pos, pos, mark_opts) self:update() - self:update_all_dependents() + self:update_dependents({children=true}) -- Marks should stay at the beginning of the snippet, only the first mark is needed. start_node.mark = self.nodes[1].mark @@ -940,6 +926,8 @@ function Snippet:fake_expand(opts) self:indent("") + self.___static_expanded = true + -- ext_opts don't matter here, just use convenient values. self.effective_child_ext_opts = self.child_ext_opts self.ext_opts = self.node_ext_opts @@ -1107,7 +1095,6 @@ function Snippet:input_leave(_, dry_run) end self:event(events.leave) - self:update_dependents() -- set own ext_opts to snippet-passive, there is no passive for snippets. self.mark:update_opts(self.ext_opts.snippet_passive) @@ -1307,23 +1294,6 @@ function Snippet:set_argnodes(dict) end end -function Snippet:update_all_dependents() - -- call the version that only updates this node. - self:_update_dependents() - -- only for insertnodes, others will not have dependents. - for _, node in ipairs(self.insert_nodes) do - node:update_all_dependents() - end -end -function Snippet:update_all_dependents_static() - -- call the version that only updates this node. - self:_update_dependents_static() - -- only for insertnodes, others will not have dependents. - for _, node in ipairs(self.insert_nodes) do - node:update_all_dependents_static() - end -end - function Snippet:resolve_position(position) -- only snippets have -1-node. if position == -1 and self.type == types.snippet then @@ -1570,6 +1540,13 @@ function Snippet:subtree_do(opts) opts.post(self) end +function Snippet:get_snippet() + if self.type == types.snippet then + return self + else + return self.parent.snippet + end +end -- affect all children nested into this snippet. function Snippet:subtree_leave_entered() @@ -1587,6 +1564,7 @@ function Snippet:subtree_leave_entered() end end end + return { Snippet = Snippet, S = S, diff --git a/lua/luasnip/nodes/textNode.lua b/lua/luasnip/nodes/textNode.lua index 602226907..3856620ec 100644 --- a/lua/luasnip/nodes/textNode.lua +++ b/lua/luasnip/nodes/textNode.lua @@ -32,8 +32,6 @@ function TextNode:input_enter(no_move, dry_run) self:event(events.enter, no_move) end -function TextNode:update_all_dependents() end - function TextNode:is_interactive() -- a resounding false. return false diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 6e06d1f0f..1db432989 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -1,4 +1,5 @@ local util = require("luasnip.util.util") +local tbl_util = require("luasnip.util.table") local ext_util = require("luasnip.util.ext_opts") local types = require("luasnip.util.types") local key_indexer = require("luasnip.nodes.key_indexer") @@ -772,6 +773,33 @@ local function nodelist_adjust_rgravs( end end +local function find_node_dependents(node) + local node_position = node.absolute_insert_position + local dict = node:get_snippet().dependents_dict + local nodes = {} + + -- this might also be called from a node which does not possess a position! + -- (for example, a functionNode may be depended upon via its key) + if node_position then + node_position[#node_position + 1] = "dependents" + vim.list_extend(nodes, dict:find_all(node_position, "dependent") or {}) + node_position[#node_position] = nil + end + + vim.list_extend( + nodes, + dict:find_all({ node, "dependents" }, "dependent") or {} + ) + + if node.key then + vim.list_extend( + nodes, + dict:find_all({ "key", node.key, "dependents" }, "dependent") or {} + ) + end + + return nodes +end local function node_subtree_do(node, opts) -- provide default-values. @@ -784,6 +812,48 @@ local function node_subtree_do(node, opts) node:subtree_do(opts) end + + +local function collect_dependents(node, which, static) + local dependents_set = {} + + if which.own then + for _, dep in ipairs(find_node_dependents(node)) do + dependents_set[dep] = true + end + end + if which.parents then + -- find dependents of all ancestors without duplicates. + local path_to_root = root_path(node, static) + -- remove `node` from path (its dependents are included if `which.own` + -- is set) + table.remove(path_to_root, 1) + for _, ancestor in ipairs(path_to_root) do + for _, dep in ipairs(find_node_dependents(ancestor)) do + dependents_set[dep] = true + end + end + end + if which.children then + -- only collects children in same snippet as node. + node_subtree_do(node, { + pre = function(st_node) + -- don't update for self. + if st_node == node then + return + end + + for _, dep in ipairs(find_node_dependents(st_node)) do + dependents_set[dep] = true + end + end, + static = static + }) + end + + return tbl_util.set_to_list(dependents_set) +end + return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -806,5 +876,7 @@ return { interactive_node = interactive_node, root_path = root_path, nodelist_adjust_rgravs = nodelist_adjust_rgravs, + find_node_dependents = find_node_dependents, + collect_dependents = collect_dependents, node_subtree_do = node_subtree_do } diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index c48273729..427d3953d 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2033,12 +2033,13 @@ describe("session", function() {2:-- INSERT --} |]], }) - -- delete snippet-text while an update for the dynamicNode is pending - -- => when the dynamicNode is left during `refocus`, the deletion will - -- be detected, and snippet removed from the jumplist. - feed("kkkVjjjjjd") + -- delete extmark manually of current node manually, to simulate an + -- issue with it. + -- => when the dynamicNode is left during `refocus`, the deletion + -- will be detected, and snippet removed from the jumplist. + exec_lua([[vim.api.nvim_buf_del_extmark(0, ls.session.ns_id, ls.session.current_nodes[1].mark.id)]]) - feed("jifn") + feed("Gofn") expand() -- make sure the snippet-roots-list is still an array, and we did not From db12345497da3085cd0f07baedabb3c7b65a83ab Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 11:48:47 +0100 Subject: [PATCH 07/69] insertNode: exit all nested snippets, not just the first. --- lua/luasnip/nodes/insertNode.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 39e9f1563..da05e4f36 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -246,8 +246,8 @@ function InsertNode:input_leave(_, dry_run) end function InsertNode:exit() - if self.inner_first then - self.inner_first:exit() + for _, snip in ipairs(self:child_snippets()) do + snip:exit() end -- reset runtime-acquired values. From e80c049def0621ecdf0f7664bd03643db426f095 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 10 Mar 2024 20:20:16 +0100 Subject: [PATCH 08/69] make sure visible is set on -1-node. put_initial is not called on it, but since it's always visible we can set it when initializing. --- lua/luasnip/nodes/snippet.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index c4a60bc32..8946cebff 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -764,6 +764,7 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) -- needed for querying node-path from snippet to this node. start_node.absolute_position = { -1 } start_node.parent = self + start_node.visible = true -- hook up i0 and start_node, and then the snippet itself. -- they are outside, not inside the snippet. From da3529bddd98280e414f7b543e03a3fce422cd34 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 24 Apr 2024 00:00:58 +0200 Subject: [PATCH 09/69] propery remove child-snippets when `:exit`ing. This is important to prevent an infinite loop when a snippet is remove_from_jumplist'd in node_util.snippettree_find_undamaged_node: if child_snippets is not modified, we just continue to remove it (or call :r_f_j but immediately nop because the snippet is not visible). --- lua/luasnip/nodes/insertNode.lua | 2 +- lua/luasnip/nodes/snippet.lua | 4 ++++ lua/luasnip/nodes/util.lua | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index da05e4f36..1473166f9 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -247,7 +247,7 @@ end function InsertNode:exit() for _, snip in ipairs(self:child_snippets()) do - snip:exit() + snip:remove_from_jumplist() end -- reset runtime-acquired values. diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 8946cebff..e29822ec9 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -474,6 +474,10 @@ function Snippet:remove_from_jumplist() -- nxt is snippet. local nxt = self.next.next + -- the advantage of remove_from_jumplist over exit is that the former + -- modifies its parents child_snippets, or the root-snippet-list. + -- Since the owners of this snippets' child_snippets are invalid anyway, we + -- don't bother modifying them. self:exit() local sibling_list = self.parent_node ~= nil diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 1db432989..ed38e1d50 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -679,6 +679,8 @@ local function snippettree_find_undamaged_node(pos, opts) -- The position of the offending snippet is returned in child_indx, -- and we can remove it here. prev_parent_children[child_indx]:remove_from_jumplist() + -- remove_from_jumplist modified prev_parent_children, don't need + -- to re-assign since we have a pointer to that table. elseif found_parent ~= nil and not found_parent:extmarks_valid() then -- found snippet damaged (the idea to sidestep the damaged snippet, -- even if no error occurred _right now_, is to ensure that we can From 9b5450ff8d442c16a022407ec68ccbff77b374bc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 24 Apr 2024 00:03:00 +0200 Subject: [PATCH 10/69] exitNode: use same update_dependents as all other nodes. --- lua/luasnip/nodes/insertNode.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 1473166f9..876d7d10c 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -92,9 +92,6 @@ function ExitNode:input_leave(no_move, dry_run) end end -function ExitNode:update_dependents() end - -function ExitNode:update_dependents_static() end function ExitNode:is_interactive() return true end From 3c0d125e1e154c362e69d1bd2b6207c4888b1fb5 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 24 Apr 2024 00:04:40 +0200 Subject: [PATCH 11/69] update after snip_expand. Has to happen because we modified text. I don't like using vim.schedule here, but it seems like this is the only way of getting the desired behaviour into all possible ways of expanding snippets (direct snip_expand is used by cmp_luasnip, so can't just handle `expand` and its variants in init.lua (or, we could do so and submit a PR to cmp_luasnip, but let's wait with that until this actually becomes problematic, which I don't think it will)). --- lua/luasnip/init.lua | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 28d230a61..bab6d022a 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -253,6 +253,17 @@ local function node_update_dependents_preserve_position(node, opts) end end +local function active_update_dependents() + local active = session.current_nodes[vim.api.nvim_get_current_buf()] + -- don't update if a jump/change_choice is in progress, or if we don't have + -- an active node. + if not session.jump_active and active ~= nil then + local upd_res = node_update_dependents_preserve_position(active, { no_move = false, restore_position = true }) + upd_res.new_node:focus() + session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_node + end +end + -- return next active node. local function safe_jump_current(dir, no_move, dry_run) local node = session.current_nodes[vim.api.nvim_get_current_buf()] @@ -434,6 +445,11 @@ local function snip_expand(snippet, opts) -- -1 to disable count. vim.cmd([[silent! call repeat#set("\luasnip-expand-repeat", -1)]]) + -- schedule update of active node. + -- Not really happy with this, but for some reason I don't have time to + -- investigate, nvim_buf_get_text does not return the updated text :/ + vim.schedule(active_update_dependents) + return snip end @@ -604,17 +620,6 @@ local function get_current_choices() return choice_lines end -local function active_update_dependents() - local active = session.current_nodes[vim.api.nvim_get_current_buf()] - -- don't update if a jump/change_choice is in progress, or if we don't have - -- an active node. - if not session.jump_active and active ~= nil then - local upd_res = node_update_dependents_preserve_position(active, { no_move = false, restore_position = true }) - upd_res.new_node:focus() - session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_node - end -end - local function store_snippet_docstrings(snippet_table) -- ensure the directory exists. -- 493 = 0755 From 7e26ca57f5f3559550f0a2b2eb43b4d1478c3f3b Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 23 Oct 2024 20:43:27 +0000 Subject: [PATCH 12/69] Format with stylua --- lua/luasnip/init.lua | 44 ++++++++++++++++------- lua/luasnip/nodes/choiceNode.lua | 2 +- lua/luasnip/nodes/dynamicNode.lua | 4 +-- lua/luasnip/nodes/functionNode.lua | 2 +- lua/luasnip/nodes/insertNode.lua | 30 ++++++++++++++-- lua/luasnip/nodes/node.lua | 12 +++++-- lua/luasnip/nodes/snippet.lua | 3 +- lua/luasnip/nodes/util.lua | 5 ++- lua/luasnip/nodes/util/snippet_string.lua | 30 ++++++++++++++++ lua/luasnip/util/str.lua | 31 ++++++++++++++++ lua/luasnip/util/util.lua | 10 ++++++ tests/integration/session_spec.lua | 4 ++- tests/unit/str_spec.lua | 21 +++++++++++ 13 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 lua/luasnip/nodes/util/snippet_string.lua diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index bab6d022a..ecfcf065c 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -155,7 +155,8 @@ local function store_cursor_node_relative(node) store_id = store_id + 1 - snip_data.cursor_end_relative = util.pos_sub(util.get_cursor_0ind(), node.mark:get_endpoint(1)) + snip_data.cursor_end_relative = + util.pos_sub(util.get_cursor_0ind(), node.mark:get_endpoint(1)) data[snip] = snip_data @@ -167,16 +168,14 @@ end local function get_corresponding_node(parent, data) return parent:find_node(function(test_node) - return (test_node.store_id == data.store_id) or (data.key ~= nil and test_node.key == data.key) + return (test_node.store_id == data.store_id) + or (data.key ~= nil and test_node.key == data.key) end) end local function restore_cursor_pos_relative(node, data) util.set_cursor_0ind( - util.pos_add( - node.mark:get_endpoint(1), - data.cursor_end_relative - ) + util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) ) end @@ -184,7 +183,8 @@ local function node_update_dependents_preserve_position(node, opts) local restore_data = store_cursor_node_relative(node) -- update all nodes that depend on this one. - local ok, res = pcall(node.update_dependents, node, {own=true, parents=true}) + local ok, res = + pcall(node.update_dependents, node, { own = true, parents = true }) if not ok then local snip = node:get_snippet() @@ -194,7 +194,10 @@ local function node_update_dependents_preserve_position(node, opts) snip.trigger, res ) - return { jump_done = false, new_node = session.current_nodes[vim.api.nvim_get_current_buf()] } + return { + jump_done = false, + new_node = session.current_nodes[vim.api.nvim_get_current_buf()], + } end -- update successful => check if the current node is still visible. @@ -228,11 +231,17 @@ local function node_update_dependents_preserve_position(node, opts) -- since the node was no longer visible after an update, it must have -- been contained in a dynamicNode, and we don't have to handle the -- case that we can't find it. - while node_parent.dynamicNode == nil or node_parent.dynamicNode.visible == false do + while + node_parent.dynamicNode == nil + or node_parent.dynamicNode.visible == false + do node_parent = node_parent.parent end local d = node_parent.dynamicNode - assert(d.active, "Visible dynamicNode that was a parent of the current node is not active after the update!! If you get this message, please open an issue with LuaSnip!") + assert( + d.active, + "Visible dynamicNode that was a parent of the current node is not active after the update!! If you get this message, please open an issue with LuaSnip!" + ) local new_node = get_corresponding_node(d, snip_restore_data) @@ -248,7 +257,10 @@ local function node_update_dependents_preserve_position(node, opts) else -- could not find corresponding node -> just jump into the -- dynamicNode that should have generated it. - return { jump_done = true, new_node = d:jump_into_snippet(opts.no_move) } + return { + jump_done = true, + new_node = d:jump_into_snippet(opts.no_move), + } end end end @@ -258,7 +270,10 @@ local function active_update_dependents() -- don't update if a jump/change_choice is in progress, or if we don't have -- an active node. if not session.jump_active and active ~= nil then - local upd_res = node_update_dependents_preserve_position(active, { no_move = false, restore_position = true }) + local upd_res = node_update_dependents_preserve_position( + active, + { no_move = false, restore_position = true } + ) upd_res.new_node:focus() session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_node end @@ -273,7 +288,10 @@ local function safe_jump_current(dir, no_move, dry_run) -- don't update for -1-node. if not dry_run and node.pos >= 0 then - local upd_res = node_update_dependents_preserve_position(node, { no_move = no_move, restore_position = false }) + local upd_res = node_update_dependents_preserve_position( + node, + { no_move = no_move, restore_position = false } + ) if upd_res.jump_done then return upd_res.new_node else diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 75686f7c1..ff7fbd0e6 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -270,7 +270,7 @@ function ChoiceNode:set_choice(choice, current_node) self.active_choice:subtree_set_pos_rgrav(to, -1, true) self.active_choice:update_restore() - self:update_dependents({own=true, parents=true, children=true}) + self:update_dependents({ own = true, parents = true, children = true }) -- Another node may have been entered in update_dependents. self:focus() diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 83c557cb3..8b26c6bfa 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -209,7 +209,7 @@ function DynamicNode:update() -- (and thus have changed text after this update), and all of the -- children's depedents (since they may have dependents outside this -- dynamicNode, who have not yet been updated) - self:update_dependents({own=true, children=true, parents=true}) + self:update_dependents({ own = true, children = true, parents = true }) end local update_errorstring = [[ @@ -298,7 +298,7 @@ function DynamicNode:update_static() tmp:update_static() -- updates own dependents. - self:update_dependents_static({own=true, parents=true, children=true}) + self:update_dependents_static({ own = true, parents = true, children = true }) end function DynamicNode:exit() diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index fdcfa0112..b7f86a1f6 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -60,7 +60,7 @@ function FunctionNode:update() -- assume that functionNode can't have a parent as its dependent, there is -- no use for that I think. - self:update_dependents({own=true, parents=true}) + self:update_dependents({ own = true, parents = true }) end local update_errorstring = [[ diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 876d7d10c..fb0f3311f 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -7,6 +7,8 @@ local types = require("luasnip.util.types") local events = require("luasnip.util.events") local extend_decorator = require("luasnip.util.extend_decorator") local feedkeys = require("luasnip.util.feedkeys") +local snippet_string = require("luasnip.nodes.util.snippet_string") +local str_util = require("luasnip.util.str") local function I(pos, static_text, opts) static_text = util.to_string_table(static_text) @@ -21,7 +23,7 @@ local function I(pos, static_text, opts) -- will only be needed for 0-node, -1-node isn't set with this. ext_gravities_active = { false, false }, inner_active = false, - input_active = false + input_active = false, }, opts) else return InsertNode:new({ @@ -31,7 +33,7 @@ local function I(pos, static_text, opts) dependents = {}, type = types.insertNode, inner_active = false, - input_active = false + input_active = false, }, opts) end end @@ -325,6 +327,30 @@ function InsertNode:subtree_leave_entered() end end +function InsertNode:get_snippetstring() + local self_from, self_to = self.mark:pos_begin_end_raw() + local text = vim.api.nvim_buf_get_text(0, self_from[1], self_from[2], self_to[1], self_to[2], {}) + + local snippetstring = snippet_string.new() + local current = {0,0} + for _, snip in ipairs(self:child_snippets()) do + local snip_from, snip_to = snip.mark:pos_begin_end_raw() + local snip_from_base_rel = util.pos_offset(self_from, snip_from) + local snip_to_base_rel = util.pos_offset(self_from, snip_to) + + snippetstring:append_text(str_util.multiline_substr(text, current, snip_from_base_rel)) + snippetstring:append_snip(snip, str_util.multiline_substr(text, snip_from_base_rel, snip_to_base_rel)) + current = snip_to_base_rel + end + snippetstring:append_text(str_util.multiline_substr(text, current, util.pos_offset(self_from, self_to))) + + return snippetstring +end + +function InsertNode:store() +end + + return { I = I, } diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 977057ee8..f97e3a3d1 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -6,6 +6,7 @@ local events = require("luasnip.util.events") local key_indexer = require("luasnip.nodes.key_indexer") local types = require("luasnip.util.types") local opt_args = require("luasnip.nodes.optional_arg") +local snippet_string = require("luasnip.nodes.util.snippet_string") local Node = {} @@ -153,6 +154,13 @@ function Node:get_text() return ok and text or { "" } end +-- if not overriden, just use get_text. +function Node:get_snippetstring() + local snipstring = snippet_string.new() + snipstring:append_text(self:get_text()) + return snipstring +end + function Node:set_old_text() self.old_text = self:get_text() end @@ -533,11 +541,11 @@ function Node:set_text(text) if self:get_snippet().___static_expanded then self.static_text = text_indented - self:update_dependents_static({own=true, parents=true}) + self:update_dependents_static({ own = true, parents = true }) else if self.visible then self:set_text_raw(text_indented) - self:update_dependents({own=true, parents=true}) + self:update_dependents({ own = true, parents = true }) end end end diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index e29822ec9..f821fe517 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -650,7 +650,6 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) -- enter current node, it will contain the new snippet. current_node:input_enter_children() end - else -- if no parent_node, completely leave. node_util.refocus(current_node, nil) @@ -760,7 +759,7 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) self.mark = mark(old_pos, pos, mark_opts) self:update() - self:update_dependents({children=true}) + self:update_dependents({ children = true }) -- Marks should stay at the beginning of the snippet, only the first mark is needed. start_node.mark = self.nodes[1].mark diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index ed38e1d50..76bac11ea 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -815,7 +815,6 @@ local function node_subtree_do(node, opts) node:subtree_do(opts) end - local function collect_dependents(node, which, static) local dependents_set = {} @@ -849,7 +848,7 @@ local function collect_dependents(node, which, static) dependents_set[dep] = true end end, - static = static + static = static, }) end @@ -880,5 +879,5 @@ return { nodelist_adjust_rgravs = nodelist_adjust_rgravs, find_node_dependents = find_node_dependents, collect_dependents = collect_dependents, - node_subtree_do = node_subtree_do + node_subtree_do = node_subtree_do, } diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua new file mode 100644 index 000000000..8870e8185 --- /dev/null +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -0,0 +1,30 @@ +local str_util = require("luasnip.util.str") + +local SnippetString = {} +local SnippetString_mt = { + __index = SnippetString, + __tostring = SnippetString.tostring +} + +local M = {} + +function M.new() + local o = {} + return setmetatable(o, SnippetString_mt) +end + +function SnippetString:append_snip(snip, str) + table.insert(self, {snip = snip, str = str}) +end +function SnippetString:append_text(str) + table.insert(self, str) +end +function SnippetString:str() + local str = {""} + for _, snipstr_or_str in ipairs(self) do + str_util.multiline_append(str, snipstr_or_str.str and snipstr_or_str.str or snipstr_or_str) + end + return str +end + +return M diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 7b8ec5f5f..ef55daa44 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -113,6 +113,36 @@ function M.sanitize(str) return str:gsub("%\r", "") end +-- requires that from and to are within the region of str. +-- str is treated as a 0,0-indexed, and the character at `to` is excluded from +-- the result. +-- `from` may not be before `to`. +function M.multiline_substr(str, from, to) + local res = {} + + -- include all rows + for i = from[1], to[1] do + table.insert(res, str[i+1]) + end + + -- trim text before from and after to. + -- First trim from behind, that way this works correctly if from and to are + -- on the same line. If res[1] was trimmed first, we'd have to adjust the + -- trim-point of `to`. + res[#res] = res[#res]:sub(1, to[2]) + res[1] = res[1]:sub(from[2]+1) + + return res +end + +-- modifies strmod +function M.multiline_append(strmod, strappend) + strmod[#strmod] = strmod[#strmod] .. strappend[1] + for i = 2, #strappend do + table.insert(strmod, strappend[i]) + end +end + -- string-operations implemented according to -- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415 -- such that they can be used for snippet-transformations in vscode-snippets. @@ -127,6 +157,7 @@ local function pascalcase(str) end return pascalcased end + M.vscode_string_modifiers = { upcase = string.upper, downcase = string.lower, diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index be4a954ea..8e71f6f5f 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -397,6 +397,15 @@ local function pos_cmp(pos1, pos2) return 2 * cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2]) end +-- compute offset of `pos` into multiline string starting at `base_pos`. +-- This is different from pos_sub because here the column-offset starts at zero +-- when `pos` is on a line different from `base_pos`. +-- Assumption: `pos` occurs after `base_pos`. +local function pos_offset(base_pos, pos) + local row_offset = pos[1] - base_pos[1] + return {row_offset, row_offset == 0 and pos[2] - base_pos[2] or pos[2]} +end + return { get_cursor_0ind = get_cursor_0ind, set_cursor_0ind = set_cursor_0ind, @@ -438,4 +447,5 @@ return { indx_of = indx_of, ternary = ternary, pos_cmp = pos_cmp, + pos_offset = pos_offset } diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index 427d3953d..964809129 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2037,7 +2037,9 @@ describe("session", function() -- issue with it. -- => when the dynamicNode is left during `refocus`, the deletion -- will be detected, and snippet removed from the jumplist. - exec_lua([[vim.api.nvim_buf_del_extmark(0, ls.session.ns_id, ls.session.current_nodes[1].mark.id)]]) + exec_lua( + [[vim.api.nvim_buf_del_extmark(0, ls.session.ns_id, ls.session.current_nodes[1].mark.id)]] + ) feed("Gofn") expand() diff --git a/tests/unit/str_spec.lua b/tests/unit/str_spec.lua index 503b4e50e..5f90aa1db 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -60,3 +60,24 @@ describe("str.unescaped_pairs", function() { { 1, 3 }, { 5, 8 }, { 9, 11 } } ) end) + +describe("str.multiline_substr", function() + -- apparently clear() needs to run before anything else... + ls_helpers.clear() + ls_helpers.exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE")) + + local function check(dscr, str, from, to, expected) + it(dscr, function() + assert.are.same(expected, exec_lua([[ + local str, from, to = ... + return require("luasnip.util.str").multiline_substr(str, from, to) + ]], str, from, to)) + end) + end + + check("entire range", {"asdf", "qwer"}, {0,0}, {1,4}, {"asdf", "qwer"}) + check("partial range", {"asdf", "qwer"}, {0,3}, {1,2}, {"f", "qw"}) + check("another partial range", {"asdf", "qwer"}, {1,2}, {1,3}, {"e"}) + check("one last partial range", {"asdf", "qwer", "zxcv"}, {0,2}, {2,4}, {"df", "qwer", "zxcv"}) + check("empty range", {"asdf", "qwer", "zxcv"}, {0,2}, {0,2}, {""}) +end) From ab84254cd1ebb7fc38489b879beea1b7dcf75b39 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 28 Oct 2024 14:33:50 +0100 Subject: [PATCH 13/69] Make insertNode correctly handle static_text if it's a snippetString. This includes `put_initial`, so a restoreNode will now store snippets expanded inside of it!! (which is really cool :D) --- lua/luasnip/nodes/choiceNode.lua | 16 ++--- lua/luasnip/nodes/dynamicNode.lua | 4 +- lua/luasnip/nodes/insertNode.lua | 71 +++++++++++++++++++-- lua/luasnip/nodes/node.lua | 4 +- lua/luasnip/nodes/restoreNode.lua | 4 +- lua/luasnip/nodes/snippet.lua | 76 +++++++++++++---------- lua/luasnip/nodes/util/snippet_string.lua | 57 ++++++++++++++++- tests/integration/function_spec.lua | 4 +- 8 files changed, 179 insertions(+), 57 deletions(-) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index ff7fbd0e6..63dffb958 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -210,12 +210,12 @@ end function ChoiceNode:setup_choice_jumps() end -function ChoiceNode:find_node(predicate) +function ChoiceNode:find_node(predicate, opts) if self.active_choice then if predicate(self.active_choice) then return self.active_choice else - return self.active_choice:find_node(predicate) + return self.active_choice:find_node(predicate, opts) end end return nil @@ -279,14 +279,14 @@ function ChoiceNode:set_choice(choice, current_node) if self.restore_cursor then local target_node = self:find_node(function(test_node) return test_node.change_choice_id == change_choice_id - end) + end, {find_in_child_snippets = true}) if target_node then - -- the node that the cursor was in when changeChoice was called exists - -- in the active choice! Enter it and all nodes between it and this choiceNode, - -- then set the cursor. - -- Pass no_move=true, we will set the cursor ourselves. - node_util.enter_nodes_between(self, target_node, true) + -- the node that the cursor was in when changeChoice was called + -- exists in the active choice! Enter it and all nodes between it + -- and this choiceNode, then set the cursor. + + node_util.refocus(self, target_node) if insert_pre_cc then util.set_cursor_0ind( diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 8b26c6bfa..4a21171d4 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -368,12 +368,12 @@ function DynamicNode:update_restore() end end -function DynamicNode:find_node(predicate) +function DynamicNode:find_node(predicate, opts) if self.snip then if predicate(self.snip) then return self.snip else - return self.snip:find_node(predicate) + return self.snip:find_node(predicate, opts) end end return nil diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index fb0f3311f..5b9e79f96 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -16,7 +16,7 @@ local function I(pos, static_text, opts) if pos == 0 then return ExitNode:new({ pos = pos, - static_text = static_text, + static_text = snippet_string.new(static_text), mark = nil, dependents = {}, type = types.exitNode, @@ -28,7 +28,7 @@ local function I(pos, static_text, opts) else return InsertNode:new({ pos = pos, - static_text = static_text, + static_text = snippet_string.new(static_text), mark = nil, dependents = {}, type = types.insertNode, @@ -260,7 +260,7 @@ end function InsertNode:get_docstring() -- copy as to not in-place-modify static text. - return util.string_wrap(self.static_text, rawget(self, "pos")) + return util.string_wrap(self:get_static_text(), rawget(self, "pos")) end function InsertNode:is_interactive() @@ -329,9 +329,19 @@ end function InsertNode:get_snippetstring() local self_from, self_to = self.mark:pos_begin_end_raw() - local text = vim.api.nvim_buf_get_text(0, self_from[1], self_from[2], self_to[1], self_to[2], {}) + -- only do one get_text, and establish relative offsets partition this + -- text. + local ok, text = pcall(vim.api.nvim_buf_get_text, 0, self_from[1], self_from[2], self_to[1], self_to[2], {}) local snippetstring = snippet_string.new() + + if not ok then + -- return empty in case of failure. + -- This may frequently occur when the snippet is `exit`ed due to + -- failure and insertNodes fetch the text in the course of `store`. + return snippetstring + end + local current = {0,0} for _, snip in ipairs(self:child_snippets()) do local snip_from, snip_to = snip.mark:pos_begin_end_raw() @@ -347,9 +357,62 @@ function InsertNode:get_snippetstring() return snippetstring end +function InsertNode:expand_tabs(tabwidth, indentstrlen) + self.static_text:expand_tabs(tabwidth, indentstrlen) +end + +function InsertNode:indent(indentstr) + self.static_text:indent(indentstr) +end + function InsertNode:store() + self.static_text = self:get_snippetstring() +end + +function InsertNode:put_initial(pos) + self.static_text:put(pos) + self.visible = true + local _, child_snippet_idx = node_util.binarysearch_pos(self.parent.snippet.child_snippets, pos, true, "outside") + for snip in self.static_text:iter_snippets() do + -- don't have to pass a current_node, we don't need it since we can + -- certainly link the snippet into this insertNode. + snip:insert_into_jumplist(nil, self, self.parent.snippet.child_snippets, child_snippet_idx) + child_snippet_idx = child_snippet_idx + 1 + end end +function InsertNode:get_static_text() + if not self.visible and not self.static_visible then + return nil + end + return self.static_text:str() +end + +function InsertNode:set_text(text) + local text_indented = util.indent(text, self.parent.indentstr) + + if self:get_snippet().___static_expanded then + self.static_text = snippet_string.new(text_indented) + self:update_dependents_static({ own = true, parents = true }) + else + if self.visible then + self:set_text_raw(text_indented) + self:update_dependents({ own = true, parents = true }) + end + end +end + +function InsertNode:find_node(predicate, opts) + if opts and opts.find_in_child_snippets then + for _, snip in ipairs(self:child_snippets()) do + local node_in_child = snip:find_node(predicate, opts) + if node_in_child then + return node_in_child + end + end + end + return nil +end return { I = I, diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index f97e3a3d1..97b01d9b0 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -193,8 +193,8 @@ function Node:update() end function Node:update_static() end -function Node:expand_tabs(tabwidth, indentstr) - util.expand_tabs(self.static_text, tabwidth, indentstr) +function Node:expand_tabs(tabwidth, indentstrlen) + util.expand_tabs(self.static_text, tabwidth, indentstrlen) end function Node:indent(indentstr) diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index c63be00bd..b07c451b6 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -222,12 +222,12 @@ function RestoreNode:update_restore() self.snip:update_restore() end -function RestoreNode:find_node(predicate) +function RestoreNode:find_node(predicate, opts) if self.snip then if predicate(self.snip) then return self.snip else - return self.snip:find_node(predicate) + return self.snip:find_node(predicate, opts) end end return nil diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index f821fe517..d24aec1f5 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -518,14 +518,15 @@ function Snippet:remove_from_jumplist() end end -local function insert_into_jumplist( - snippet, - start_node, +function Snippet:insert_into_jumplist( current_node, parent_node, sibling_snippets, own_indx ) + -- this is always the case. + local start_node = self.prev + local prev_snippet = sibling_snippets[own_indx - 1] -- have not yet inserted self!! local next_snippet = sibling_snippets[own_indx] @@ -558,13 +559,13 @@ local function insert_into_jumplist( -- in all cases if link_children and prev ~= nil then -- if we have a previous snippet we can link to, just do that. - prev.next.next = snippet + prev.next.next = self start_node.prev = prev.insert_nodes[0] else -- only jump from parent to child if link_children is set. if link_children then -- prev is nil, but we can link up using the parent. - parent_node.inner_first = snippet + parent_node.inner_first = self end -- make sure we can jump back to the parent. start_node.prev = parent_node @@ -573,14 +574,14 @@ local function insert_into_jumplist( -- exact same reasoning here as in prev-case above, omitting comments. if link_children and next ~= nil then -- jump from next snippets start_node to $0. - next.prev.prev = snippet.insert_nodes[0] + next.prev.prev = self.insert_nodes[0] -- jump from $0 to next snippet (skip its start_node) - snippet.insert_nodes[0].next = next + self.insert_nodes[0].next = next else if link_children then - parent_node.inner_last = snippet.insert_nodes[0] + parent_node.inner_last = self.insert_nodes[0] end - snippet.insert_nodes[0].next = parent_node + self.insert_nodes[0].next = parent_node end else -- naively, even if the parent is linkable, there might be snippets @@ -599,23 +600,23 @@ local function insert_into_jumplist( -- previous history, and we don't mess up whatever jumps -- are set up around current_node) start_node.prev = current_node - snippet.insert_nodes[0].next = current_node + self.insert_nodes[0].next = current_node end -- don't link different root-nodes for unlinked_roots. elseif link_roots then -- inserted into top-level snippet-forest, just hook up with prev, next. -- prev and next have to be snippets or nil, in this case. if prev ~= nil then - prev.next.next = snippet + prev.next.next = self start_node.prev = prev.insert_nodes[0] end if next ~= nil then - snippet.insert_nodes[0].next = next - next.prev.prev = snippet.insert_nodes[0] + self.insert_nodes[0].next = next + next.prev.prev = self.insert_nodes[0] end end - table.insert(sibling_snippets, own_indx, snippet) + table.insert(sibling_snippets, own_indx, self) end function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) @@ -748,21 +749,6 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) end local start_node = iNode.I(0) - - local old_pos = vim.deepcopy(pos) - self:put_initial(pos) - - local mark_opts = vim.tbl_extend("keep", { - right_gravity = false, - end_right_gravity = false, - }, self:get_passive_ext_opts()) - self.mark = mark(old_pos, pos, mark_opts) - - self:update() - self:update_dependents({ children = true }) - - -- Marks should stay at the beginning of the snippet, only the first mark is needed. - start_node.mark = self.nodes[1].mark start_node.pos = -1 -- needed for querying node-path from snippet to this node. start_node.absolute_position = { -1 } @@ -782,9 +768,12 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) -- parent_node is nil if the snippet is toplevel. self.parent_node = parent_node - insert_into_jumplist( - self, - start_node, + self:put(pos) + + self:update() + self:update_dependents({ children = true }) + + self:insert_into_jumplist( current_node, parent_node, sibling_snippets, @@ -1263,17 +1252,20 @@ function Snippet:get_pattern_expand_helper() return self.expand_helper_snippet end -function Snippet:find_node(predicate) +function Snippet:find_node(predicate, opts) for _, node in ipairs(self.nodes) do if predicate(node) then return node else - local node_in_child = node:find_node(predicate) + local node_in_child = node:find_node(predicate, opts) if node_in_child then return node_in_child end end end + if predicate(self.prev) then + return self.prev + end return nil end @@ -1569,6 +1561,22 @@ function Snippet:subtree_leave_entered() end end +function Snippet:put(pos) + --- Put text-content of snippet into buffer and set marks. + local old_pos = vim.deepcopy(pos) + self:put_initial(pos) + + local mark_opts = vim.tbl_extend("keep", { + right_gravity = false, + end_right_gravity = false, + }, self:get_passive_ext_opts()) + self.mark = mark(old_pos, pos, mark_opts) + + -- The start_nodes' marks should stay at the beginning of the snippet, only + -- the first mark is needed. + self.prev.mark = self.nodes[1].mark +end + return { Snippet = Snippet, S = S, diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 8870e8185..65c51fe0d 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -1,15 +1,20 @@ local str_util = require("luasnip.util.str") +local util = require("luasnip.util.util") +---@class SnippetString local SnippetString = {} local SnippetString_mt = { __index = SnippetString, - __tostring = SnippetString.tostring + __tostring = SnippetString.str } local M = {} -function M.new() - local o = {} +---Create new SnippetString. +---@param initial_str string[]?, optional initial multiline string. +---@return SnippetString +function M.new(initial_str) + local o = {initial_str} return setmetatable(o, SnippetString_mt) end @@ -27,4 +32,50 @@ function SnippetString:str() return str end +function SnippetString:indent(indentstr) + for _, snipstr_or_str in ipairs(self) do + if snipstr_or_str.snip then + snipstr_or_str.snip:indent(indentstr) + util.indent(snipstr_or_str.str, indentstr) + else + util.indent(snipstr_or_str, indentstr) + end + end +end + +function SnippetString:expand_tabs(tabwidth, indenstrlen) + for _, snipstr_or_str in ipairs(self) do + if snipstr_or_str.snip then + snipstr_or_str.snip:expand_tabs(tabwidth, indenstrlen) + util.expand_tabs(snipstr_or_str.str, tabwidth, indenstrlen) + else + util.expand_tabs(snipstr_or_str, tabwidth, indenstrlen) + end + end +end + +function SnippetString:iter_snippets() + local i = 1 + return function() + -- find the next snippet. + while self[i] and (not self[i].snip) do + i = i+1 + end + local res = self[i] and self[i].snip + i = i+1 + return res + end +end + +-- pos is modified to reflect the new cursor-position! +function SnippetString:put(pos) + for _, snipstr_or_str in ipairs(self) do + if snipstr_or_str.snip then + snipstr_or_str.snip:put(pos) + else + util.put(snipstr_or_str, pos) + end + end +end + return M diff --git a/tests/integration/function_spec.lua b/tests/integration/function_spec.lua index 64463927e..1476ef364 100644 --- a/tests/integration/function_spec.lua +++ b/tests/integration/function_spec.lua @@ -180,8 +180,8 @@ describe("FunctionNode", function() }) ]] assert.are.same( - exec_lua("return " .. snip .. ":get_static_text()"), - { "cccc aaaa" } + { "cccc aaaa" }, + exec_lua("return " .. snip .. ":get_static_text()") ) -- the functionNode shouldn't be evaluated after expansion, the ai[2][2] isn't available. exec_lua("ls.snip_expand(" .. snip .. ")") From d08cf3036eeb056e38a305c8850590b985088db8 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 28 Oct 2024 21:04:54 +0100 Subject: [PATCH 14/69] allow using snippet_string as dynamicNode-args. This allows us to duplicate snippets within argnodes into the dynamicNode. --- lua/luasnip/nodes/dynamicNode.lua | 33 +++++++++++----- lua/luasnip/nodes/functionNode.lua | 9 +++-- lua/luasnip/nodes/insertNode.lua | 38 +++++++++++++++--- lua/luasnip/nodes/node.lua | 18 ++++++--- lua/luasnip/nodes/util.lua | 8 ++++ lua/luasnip/nodes/util/snippet_string.lua | 47 +++++++++++++++++++++++ lua/luasnip/util/mark.lua | 9 ++++- 7 files changed, 136 insertions(+), 26 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 4a21171d4..baad069ef 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -18,6 +18,7 @@ local function D(pos, fn, args, opts) type = types.dynamicNode, mark = nil, user_args = opts.user_args or {}, + snippetstring_args = opts.snippetstring_args or false, dependents = {}, active = false, }, opts) @@ -118,7 +119,10 @@ end function DynamicNode:update() local args = self:get_args() - if vim.deep_equal(self.last_args, args) then + local str_args = node_util.str_args(args) + local effective_args = self.snippetstring_args and args or str_args + + if vim.deep_equal(self.last_args, str_args) then -- no update, the args still match. return end @@ -136,7 +140,7 @@ function DynamicNode:update() -- build new snippet before exiting, markers may be needed for construncting. tmp = self.fn( - args, + effective_args, self.parent, self.snip.old_state, unpack(self.user_args) @@ -150,14 +154,17 @@ function DynamicNode:update() else self:focus() if not args then - -- no snippet exists, set an empty one. + -- not all args are available => set to empty snippet. tmp = SnippetNode(nil, {}) else -- also enter node here. - tmp = self.fn(args, self.parent, nil, unpack(self.user_args)) + tmp = self.fn(effective_args, self.parent, nil, unpack(self.user_args)) end end - self.last_args = args + + -- make sure update only when text changed, not if there was just some kind + -- of metadata-modification of one of the snippets. + self.last_args = str_args -- act as if snip is directly inside parent. tmp.parent = self.parent @@ -219,7 +226,10 @@ Error while evaluating dynamicNode@%d for snippet '%s': :h luasnip-docstring for more info]] function DynamicNode:update_static() local args = self:get_static_args() - if vim.deep_equal(self.last_static_args, args) then + local str_args = node_util.str_args(args) + local effective_args = self.snippetstring_args and args or str_args + + if vim.deep_equal(self.last_static_args, str_args) then -- no update, the args still match. return end @@ -234,7 +244,7 @@ function DynamicNode:update_static() -- build new snippet before exiting, markers may be needed for construncting. ok, tmp = pcall( self.fn, - args, + effective_args, self.parent, self.snip.old_state, unpack(self.user_args) @@ -246,7 +256,7 @@ function DynamicNode:update_static() else -- also enter node here. ok, tmp = - pcall(self.fn, args, self.parent, nil, unpack(self.user_args)) + pcall(self.fn, effective_args, self.parent, nil, unpack(self.user_args)) end end if not ok then @@ -256,7 +266,7 @@ function DynamicNode:update_static() -- set empty snippet on failure tmp = SnippetNode(nil, {}) end - self.last_static_args = args + self.last_static_args = str_args -- act as if snip is directly inside parent. tmp.parent = self.parent @@ -331,7 +341,10 @@ end function DynamicNode:update_restore() -- only restore snippet if arg-values still match. - if self.stored_snip and vim.deep_equal(self:get_args(), self.last_args) then + local args = self:get_args() + local str_args = node_util.str_args(args) + + if self.stored_snip and vim.deep_equal(str_args, self.last_args) then local tmp = self.stored_snip tmp.mark = diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index b7f86a1f6..0fec9c258 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -7,6 +7,7 @@ local tNode = require("luasnip.nodes.textNode").textNode local extend_decorator = require("luasnip.util.extend_decorator") local key_indexer = require("luasnip.nodes.key_indexer") local opt_args = require("luasnip.nodes.optional_arg") +local snippet_string = require("luasnip.nodes.util.snippet_string") local function F(fn, args, opts) opts = opts or {} @@ -36,7 +37,7 @@ end FunctionNode.get_docstring = FunctionNode.get_static_text function FunctionNode:update() - local args = self:get_args() + local args = node_util.str_args(self:get_args()) -- skip this update if -- - not all nodes are available. -- - the args haven't changed. @@ -69,7 +70,8 @@ Error while evaluating functionNode@%d for snippet '%s': :h luasnip-docstring for more info]] function FunctionNode:update_static() - local args = self:get_static_args() + local args = node_util.str_args(self:get_static_args()) + -- skip this update if -- - not all nodes are available. -- - the args haven't changed. @@ -97,8 +99,9 @@ function FunctionNode:update_static() end function FunctionNode:update_restore() + local args = node_util.str_args(self:get_args()) -- only if args still match. - if self.static_text and vim.deep_equal(self:get_args(), self.last_args) then + if self.static_text and vim.deep_equal(args, self.last_args) then self:set_text_raw(self.static_text) else self:update() diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 5b9e79f96..29344f1cf 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -11,12 +11,14 @@ local snippet_string = require("luasnip.nodes.util.snippet_string") local str_util = require("luasnip.util.str") local function I(pos, static_text, opts) - static_text = util.to_string_table(static_text) + if not snippet_string.isinstance(static_text) then + static_text = snippet_string.new(util.to_string_table(static_text)) + end + local node if pos == 0 then - return ExitNode:new({ + node = ExitNode:new({ pos = pos, - static_text = snippet_string.new(static_text), mark = nil, dependents = {}, type = types.exitNode, @@ -26,9 +28,8 @@ local function I(pos, static_text, opts) input_active = false, }, opts) else - return InsertNode:new({ + node = InsertNode:new({ pos = pos, - static_text = snippet_string.new(static_text), mark = nil, dependents = {}, type = types.insertNode, @@ -36,6 +37,12 @@ local function I(pos, static_text, opts) input_active = false, }, opts) end + + -- make static text owned by this insertNode. + -- This includes copying it so that it is separate from the snippets that + -- were potentially captured in `get_args`. + node.static_text = static_text:reown(node) + return node end extend_decorator.register(I, { arg_indx = 3 }) @@ -328,6 +335,10 @@ function InsertNode:subtree_leave_entered() end function InsertNode:get_snippetstring() + if not self.visible then + return nil + end + local self_from, self_to = self.mark:pos_begin_end_raw() -- only do one get_text, and establish relative offsets partition this -- text. @@ -356,6 +367,12 @@ function InsertNode:get_snippetstring() return snippetstring end +function InsertNode:get_static_snippetstring() + if not self.visible and not self.static_visible then + return nil + end + return self.static_text +end function InsertNode:expand_tabs(tabwidth, indentstrlen) self.static_text:expand_tabs(tabwidth, indentstrlen) @@ -372,7 +389,16 @@ end function InsertNode:put_initial(pos) self.static_text:put(pos) self.visible = true - local _, child_snippet_idx = node_util.binarysearch_pos(self.parent.snippet.child_snippets, pos, true, "outside") + local _, child_snippet_idx = node_util.binarysearch_pos( + self.parent.snippet.child_snippets, + pos, + -- we are always focused on this node when this is called (I'm pretty + -- sure at least), so we should follow the gravity when finding this + -- index. + true, + -- try to enter snippets I guess. + node_util.binarysearch_preference.inside) + for snip in self.static_text:iter_snippets() do -- don't have to pass a current_node, we don't need it since we can -- certainly link the snippet into this insertNode. diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 97b01d9b0..1c7e903ef 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -154,11 +154,17 @@ function Node:get_text() return ok and text or { "" } end --- if not overriden, just use get_text. function Node:get_snippetstring() - local snipstring = snippet_string.new() - snipstring:append_text(self:get_text()) - return snipstring + -- if this is not overridden, get_text returns a multiline string. + return snippet_string.new(self:get_text()) +end + +function Node:get_static_snippetstring() + if not self.visible and not self.static_visible then + return nil + end + -- if this is not overridden, get_static_text() is a multiline string. + return snippet_string.new(self:get_static_text()) end function Node:set_old_text() @@ -291,10 +297,10 @@ local function get_args(node, get_text_func_name) end function Node:get_args() - return get_args(self, "get_text") + return get_args(self, "get_snippetstring") end function Node:get_static_args() - return get_args(self, "get_static_text") + return get_args(self, "get_static_snippetstring") end function Node:get_jump_index() diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 76bac11ea..86465f962 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -5,6 +5,7 @@ local types = require("luasnip.util.types") local key_indexer = require("luasnip.nodes.key_indexer") local session = require("luasnip.session") local feedkeys = require("luasnip.util.feedkeys") +local snippet_string = require("luasnip.nodes.util.snippet_string") local function subsnip_init_children(parent, children) for _, child in ipairs(children) do @@ -855,6 +856,12 @@ local function collect_dependents(node, which, static) return tbl_util.set_to_list(dependents_set) end +local function str_args(args) + return args and vim.tbl_map(function(arg) + return snippet_string.isinstance(arg) and arg:str() or arg + end, args) +end + return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -880,4 +887,5 @@ return { find_node_dependents = find_node_dependents, collect_dependents = collect_dependents, node_subtree_do = node_subtree_do, + str_args = str_args } diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 65c51fe0d..adc0a5b41 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -18,6 +18,10 @@ function M.new(initial_str) return setmetatable(o, SnippetString_mt) end +function M.isinstance(o) + return getmetatable(o) == SnippetString_mt +end + function SnippetString:append_snip(snip, str) table.insert(self, {snip = snip, str = str}) end @@ -78,4 +82,47 @@ function SnippetString:put(pos) end end +function SnippetString:reown(new_parent) + -- on 0.7 vim.deepcopy does not behave correctly => have to manually copy. + return setmetatable(vim.tbl_map(function(snipstr_or_str) + if snipstr_or_str.snip then + local snip = snipstr_or_str.snip + + -- remove associations with objects beyond this snippet. + -- This is so we can easily deepcopy it without copying too much data. + -- We could also do this copy in + local prevprev = snip.prev.prev + local i0next = snip.insert_nodes[0].next + local parentnode = snip.parent_node + + snip.prev.prev = nil + snip.insert_nodes[0].next = nil + snip.parent_node = nil + + local snipcop = snip:copy() + + snip.prev.prev = prevprev + snip.insert_nodes[0].next = i0next + snip.parent_node = parentnode + + + -- bring into inactive mode, so that we will jump into it correctly when it + -- is expanded again. + snipcop:subtree_do({ + pre = function(node) + node.mark:invalidate() + end, + post = util.nop + }) + snipcop:exit() + -- set correct parent_node. + snipcop.parent_node = new_parent + + return {snip = snipcop, str = vim.deepcopy(snipstr_or_str.str)} + else + return vim.deepcopy(snipstr_or_str) + end + end, self), SnippetString_mt) +end + return M diff --git a/lua/luasnip/util/mark.lua b/lua/luasnip/util/mark.lua index daf9b074a..fe5d0bb15 100644 --- a/lua/luasnip/util/mark.lua +++ b/lua/luasnip/util/mark.lua @@ -199,8 +199,15 @@ function Mark:update_opts(opts) self:set_opts(opts_cp) end +-- invalidate this mark object only, leave the underlying extmark alone. +function Mark:invalidate() + self.id = nil +end + function Mark:clear() - vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id) + if self.id then + vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id) + end end return { From f8b7d1a9569a06fbb37f649e796a802ea0579ca6 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:09:40 +0100 Subject: [PATCH 15/69] restoreNode,insertNode: propagate store. If store is triggered manually (for example in dynamicNode:update), it should also be performed for the entire snippetTree! --- lua/luasnip/nodes/insertNode.lua | 3 +++ lua/luasnip/nodes/restoreNode.lua | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 29344f1cf..60783686d 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -383,6 +383,9 @@ function InsertNode:indent(indentstr) end function InsertNode:store() + for _, snip in ipairs(self:child_snippets()) do + snip:store() + end self.static_text = self:get_snippetstring() end diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index b07c451b6..fd65a14bc 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -215,7 +215,11 @@ function RestoreNode:get_docstring() return self.docstring end -function RestoreNode:store() end +function RestoreNode:store() + if self.snip then + self.snip:store() + end +end -- will be restored through other means. function RestoreNode:update_restore() From 3a7c677b269b6cee391c04e59568eed0f0533d54 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:20:50 +0100 Subject: [PATCH 16/69] dynamicNode.update: store snippet before evaluating fn. --- lua/luasnip/nodes/dynamicNode.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index baad069ef..e50cbce79 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -138,6 +138,14 @@ function DynamicNode:update() return end + -- make sure all nodes store their up-to-date content. + -- This is relevant if an argnode contains a snippet which contains a + -- restoreNode: the snippet will be copied and the `self.snip:exit` + -- will cause a store for the original snippet, but not the copy that + -- may be inserted into `tmp` by `self.fn`. + self.snip:store() + self.snip:subtree_leave_entered() + -- build new snippet before exiting, markers may be needed for construncting. tmp = self.fn( effective_args, @@ -145,7 +153,7 @@ function DynamicNode:update() self.snip.old_state, unpack(self.user_args) ) - self.snip:subtree_leave_entered() + self.snip:exit() self.snip = nil From 8bd88b24e7a5ac6c3f3ec3ba16898a3ac70ba6df Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:22:57 +0100 Subject: [PATCH 17/69] dynamicNode.update: copy extmarks after focusing. --- lua/luasnip/nodes/dynamicNode.lua | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index e50cbce79..a8fd569e1 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -187,8 +187,6 @@ function DynamicNode:update() tmp:resolve_node_ext_opts() tmp:subsnip_init() - tmp.mark = - self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) tmp.dynamicNode = self tmp:init_positions(self.snip_absolute_position) @@ -205,7 +203,13 @@ function DynamicNode:update() tmp:indent(self.parent.indentstr) -- sets own extmarks false,true + -- focus and then set snippetNode-gravity => make sure that + -- snippetNode-extmark is shifted correctly. self:focus() + + tmp.mark = + self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) + local from, to = self.mark:pos_begin_end_raw() -- inserts nodes with extmarks false,false tmp:put_initial(from) @@ -355,9 +359,6 @@ function DynamicNode:update_restore() if self.stored_snip and vim.deep_equal(str_args, self.last_args) then local tmp = self.stored_snip - tmp.mark = - self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) - -- position might (will probably!!) still have changed, so update it -- here too (as opposed to only in update). tmp:init_positions(self.snip_absolute_position) @@ -370,7 +371,9 @@ function DynamicNode:update_restore() -- sets own extmarks false,true self:focus() - -- inserts nodes with extmarks false,false + tmp.mark = + self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) + local from, to = self.mark:pos_begin_end_raw() tmp:put_initial(from) -- adjust gravity in left side of snippet, such that it matches the current From 8c0bb5965d95193a514d834563a7222c10392807 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:23:22 +0100 Subject: [PATCH 18/69] dynamicNode.update: do update_restore instead of update. This enables restoring the dynamicNode content in snippetStrings. --- lua/luasnip/nodes/dynamicNode.lua | 3 ++- lua/luasnip/nodes/insertNode.lua | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index a8fd569e1..f5efd4394 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -223,7 +223,8 @@ function DynamicNode:update() -- Both are needed, because -- - a node could only depend on nodes outside of tmp -- - a node outside of tmp could depend on one inside of tmp - tmp:update() + tmp:update_restore() + -- update nodes that depend on this dynamicNode, nodes that are parents -- (and thus have changed text after this update), and all of the -- children's depedents (since they may have dependents outside this diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 60783686d..a669bf793 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -443,6 +443,12 @@ function InsertNode:find_node(predicate, opts) return nil end +function InsertNode:update_restore() + for _, snip in pairs(self:child_snippets()) do + snip:update_restore() + end +end + return { I = I, } From 6f39702fb0f4c414285e258eedd9433d3fb1e03f Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:51:19 +0100 Subject: [PATCH 19/69] restoreNode: don't store on exit, store should have been called before. exit is also called when a snippet should be deleted due to invalid extmarks, if we do something like store in there, we have to always check extmarks. For now, leave a pcall in get_snippetstring, get_text behaved the same. But log errors! --- lua/luasnip/nodes/insertNode.lua | 4 ++-- lua/luasnip/nodes/restoreNode.lua | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index a669bf793..21bbb471b 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -9,6 +9,7 @@ local extend_decorator = require("luasnip.util.extend_decorator") local feedkeys = require("luasnip.util.feedkeys") local snippet_string = require("luasnip.nodes.util.snippet_string") local str_util = require("luasnip.util.str") +local log = require("luasnip.util.log").new("insertNode") local function I(pos, static_text, opts) if not snippet_string.isinstance(static_text) then @@ -347,9 +348,8 @@ function InsertNode:get_snippetstring() local snippetstring = snippet_string.new() if not ok then + log.warn("Failure while getting text of insertNode: " .. text) -- return empty in case of failure. - -- This may frequently occur when the snippet is `exit`ed due to - -- failure and insertNodes fetch the text in the course of `store`. return snippetstring end diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index fd65a14bc..8519eb0d5 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -36,8 +36,7 @@ function RestoreNode:exit() self.visible = false self.mark:clear() - -- snip should exist if exit is called. - self.snip:store() + -- will be copied on restore, no need to copy here too. self.parent.snippet.stored[self.key] = self.snip self.snip:exit() From fc94ef20a8eece2002a9ffba4a958f47bb64e219 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 12:15:19 +0100 Subject: [PATCH 20/69] add some tests for new restoreNode-behaviour. --- tests/integration/restore_spec.lua | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/integration/restore_spec.lua b/tests/integration/restore_spec.lua index b3903d466..196dac05c 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -354,4 +354,110 @@ describe("RestoreNode", function() {2:-- SELECT --} |]], }) end) + + it("correctly restores snippets (1).", function() + exec_lua([[ + ls.snip_expand(s("trig", { + c(1, { + sn(nil, {t"a: ", r(1, "key", i(1, "asdf"))}), + sn(nil, {t"b: ", r(1, "key")}), + }, {restore_cursor = true}) + })) + ]]) + + feed(". .") + exec_lua("ls.lsp_expand('($1)')") +screen:expect({ + grid = [[ + a: . (^) . | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua("ls.change_choice(1)") +screen:expect({ + grid = [[ + b: . (^) . | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + end) + + it("correctly restores snippets (2).", function() + + exec_lua([[ + ls.setup({link_children = true}) + ls.snip_expand(s("trig", { + i(1, "asdf"), t" ", d(2, function(args) + return sn(nil, { + r(1, "key", i(1, "qq")), + i(2, args[1]) + }) + end, {1}) + })) + ]]) + exec_lua[[ls.jump(1)]] + feed(". .") + exec_lua("ls.lsp_expand('($1)')") + feed("i") +screen:expect({ + grid = [[ + asdf . (i^) .asdf | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua("ls.jump(-1) ls.jump(-1)") + feed("qwer") + exec_lua("ls.jump(1) ls.jump(1)") +screen:expect({ + grid = [[ + qwer . (^i) .qwer | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + end) + + -- make sure store and update_restore propagate. + it("correctly restores snippets (3).", function() + + exec_lua([[ + ls.setup({link_children = true}) + ls.snip_expand(s("trig", { + i(1, "asdf"), t" ", d(2, function(args) + return sn(nil, { + r(1, "key", i(1, "qq")), + i(2, args[1]) + }) + end, {1}) + })) + ]]) + exec_lua[[ls.jump(1)]] + feed(". .") + exec_lua([[ + ls.snip_expand(s("trig", { + t("("), r(1, "inside_pairs", dl(1, l.LS_SELECT_DEDENT)), t(")") + })) + ]]) + feed("i") +screen:expect({ + grid = [[ + asdf . (i^) .asdf | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua("ls.jump(-1) ls.jump(-1)") + feed("qwer") + exec_lua("ls.jump(1) ls.jump(1)") +screen:expect({ + grid = [[ + qwer . (^i) .qwer | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + end) end) From 65b9e7e5c6bd16d0da296a62ff515ed7a0beef42 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:16:48 +0100 Subject: [PATCH 21/69] choiceNode: correctly refocus when current_node is in another snippet. --- lua/luasnip/nodes/choiceNode.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 63dffb958..ccd4d28c7 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -238,8 +238,9 @@ function ChoiceNode:set_choice(choice, current_node) self.active_choice:store() -- tear down current choice. - -- leave all so the choice (could be a snippet) is in the correct state for the next enter. - node_util.leave_nodes_between(self.active_choice, current_node) + -- leave all so the choice (could be a snippet) is in the correct state for + -- the next enter. + node_util.refocus(current_node, self.active_choice) self.active_choice:exit() From 4f52fd2da5016400a2d935fb0a8613d9f55250c1 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:17:17 +0100 Subject: [PATCH 22/69] we don't want to go into adjacent snippetNodes, but land between them. --- lua/luasnip/nodes/insertNode.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 21bbb471b..39f9d40db 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -399,8 +399,8 @@ function InsertNode:put_initial(pos) -- sure at least), so we should follow the gravity when finding this -- index. true, - -- try to enter snippets I guess. - node_util.binarysearch_preference.inside) + -- don't enter snippets, we want to find the position of this node. + node_util.binarysearch_preference.outside) for snip in self.static_text:iter_snippets() do -- don't have to pass a current_node, we don't need it since we can From afa887c0ee723a42dc61977e8e3ff76d413b4370 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:17:47 +0100 Subject: [PATCH 23/69] snippet: correctly propagate exit to child_snippets (and clear them). --- lua/luasnip/nodes/snippet.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index d24aec1f5..706caad5a 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1133,12 +1133,12 @@ end function Snippet:exit() if self.type == types.snippet then - -- if exit is called, this will not be visited again. - -- Thus, also clean up the child-snippets, which will also not be - -- visited again, since they can only be visited through self. - for _, child in ipairs(self.child_snippets) do - child:exit() + -- insertNode also call exit for their child_snippets, but if we + -- :exit() the whole snippet we can just remove all of them here. + for _, snip in ipairs(self.child_snippets) do + snip:exit() end + self.child_snippets = {} end self.visible = false From a78be522ea44a874eb6663e0d05f060fe7641ad3 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 17:55:15 +0100 Subject: [PATCH 24/69] add another test for the new restoreNode. --- tests/integration/restore_spec.lua | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/integration/restore_spec.lua b/tests/integration/restore_spec.lua index 196dac05c..9a77cd5cf 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -458,6 +458,95 @@ screen:expect({ {0:~ }| {2:-- SELECT --} | ]] +}) + end) + + -- make sure store and update_restore propagate. + it("correctly restores snippets (3).", function() + + exec_lua([[ + ls.setup({link_children = true}) + ls.snip_expand(s("trig", { + i(1, "asdf"), t" ", d(2, function(args) + return sn(nil, { + r(1, "key", i(1)), + i(2, args[1]) + }) + end, {1}) + })) + ]]) + exec_lua[[ls.jump(1)]] + + local function exp() + exec_lua([[ + ls.snip_expand(s("trig", { + t("("), r(1, "inside_pairs", dl(1, l.LS_SELECT_DEDENT)), t(")") + })) + ]]) + feed("i") + end + + exp() + exec_lua"ls.jump(1)" + exp() + exec_lua"ls.jump(1)" + exp() + feed("i") + exp() + exp() + exp() +screen:expect({ + grid = [[ + asdf (i)(i)(i (i(i(i^))) i)asdf | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + -- 11x to get back to the i1. + exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" + exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" + exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" + exec_lua"ls.jump(-1) ls.jump(-1)" + feed("qwer") + exec_lua"ls.jump(1)" +screen:expect({ + grid = [[ + qwer ^({3:i)(i)(i (i(i(i))) i)}qwer | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" +screen:expect({ + grid = [[ + qwer (i)(^i)(i (i(i(i))) i)qwer | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" +screen:expect({ + grid = [[ + qwer (i)(i)(i (^i{3:(i(i))}) i)qwer | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" +screen:expect({ + grid = [[ + qwer (i)(i)(i (i(i(i)^)) i)qwer | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua"ls.jump(1) ls.jump(1) ls.jump(1) ls.jump(1)" +screen:expect({ + grid = [[ + qwer (i)(i)(i (i(i(i))) i)^q{3:wer} | + {0:~ }| + {2:-- SELECT --} | + ]] }) end) end) From eb9cefd8bf9583a78ecec695df8afe90382545b8 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 21:21:14 +0100 Subject: [PATCH 25/69] store content of nested snippets before capturing argnode. --- lua/luasnip/nodes/node.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 1c7e903ef..78d7685db 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -276,6 +276,13 @@ local function get_args(node, get_text_func_name) end -- maybe the node is part of a dynamicNode and not yet generated. if argnode then + -- now, store traverses the whole tree, and if one argnode includes + -- another we'd duplicate some work. + -- But I don't think there's a really good reason for doing + -- something like this (we already have all the data by capturing + -- the outer argnode), and even if it happens, it should occur only + -- rarely. + argnode:store() local argnode_text = argnode[get_text_func_name](argnode) -- can only occur with `get_text`. If one returns nil, the argnode -- isn't visible or some other error occured. Either way, return nil From 1688ba3450080022116de684be711f590355cb78 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 21:53:07 +0100 Subject: [PATCH 26/69] make sure marks are invalidated even for nested snippets. --- lua/luasnip/nodes/insertNode.lua | 10 ++++++++++ lua/luasnip/nodes/util/snippet_string.lua | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 39f9d40db..2b2851e36 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -449,6 +449,16 @@ function InsertNode:update_restore() end end +function InsertNode:subtree_do(opts) + opts.pre(self) + if opts.do_child_snippets then + for _, snip in ipairs(self:child_snippets()) do + snip:subtree_do(opts) + end + end + opts.post(self) +end + return { I = I, } diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index adc0a5b41..3f273d322 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -112,7 +112,8 @@ function SnippetString:reown(new_parent) pre = function(node) node.mark:invalidate() end, - post = util.nop + post = util.nop, + do_child_snippets = true }) snipcop:exit() -- set correct parent_node. From 6fa5d02787a156c20926e3398a5991db35a443d1 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 23:05:20 +0100 Subject: [PATCH 27/69] get_args: `store` only when calling in static mode. weirdly that one test breaks, and only on nvim0.7.. I don't think it's an actual bug in luasnip. so just skipping that test for now. --- lua/luasnip/nodes/node.lua | 27 +++++++++++++++++---------- tests/integration/choice_spec.lua | 2 +- tests/integration/session_spec.lua | 13 +++++++++++-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 78d7685db..e37055c89 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -243,7 +243,7 @@ function Node:event(event) }) end -local function get_args(node, get_text_func_name) +local function get_args(node, get_text_func_name, static) local argnodes_text = {} for key, arg in ipairs(node.args_absolute) do local is_optional = opt_args.is_opt(arg) @@ -276,13 +276,20 @@ local function get_args(node, get_text_func_name) end -- maybe the node is part of a dynamicNode and not yet generated. if argnode then - -- now, store traverses the whole tree, and if one argnode includes - -- another we'd duplicate some work. - -- But I don't think there's a really good reason for doing - -- something like this (we already have all the data by capturing - -- the outer argnode), and even if it happens, it should occur only - -- rarely. - argnode:store() + if not static and argnode.visible then + -- Don't store (aka call get_snippetstring) if this is a static + -- update (there will be no associated buffer-region!) and + -- don't store if the node is not visible. (Then there's + -- nothing to store anyway) + + -- now, store traverses the whole tree, and if one argnode includes + -- another we'd duplicate some work. + -- But I don't think there's a really good reason for doing + -- something like this (we already have all the data by capturing + -- the outer argnode), and even if it happens, it should occur only + -- rarely. + argnode:store() + end local argnode_text = argnode[get_text_func_name](argnode) -- can only occur with `get_text`. If one returns nil, the argnode -- isn't visible or some other error occured. Either way, return nil @@ -304,10 +311,10 @@ local function get_args(node, get_text_func_name) end function Node:get_args() - return get_args(self, "get_snippetstring") + return get_args(self, "get_snippetstring", false) end function Node:get_static_args() - return get_args(self, "get_static_snippetstring") + return get_args(self, "get_static_snippetstring", true) end function Node:get_jump_index() diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index efcb8cb14..34667c004 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -209,7 +209,7 @@ describe("ChoiceNode", function() {2:-- SELECT --} |]], }) assert.are.same(exec_lua("return ls.get_current_choices()"), { - "${${1:a}}", + "${${1:b}}", "none", }) diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index 964809129..1b71a81b5 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -380,8 +380,17 @@ describe("session", function() }) -- delete whole buffer. feed("ggVGd") - -- should not cause an error. - jump(1) + -- another jump should not cause an error. + -- for some reason this hangs indefinitely on nvim0.7, but not 0.9 or master. + -- I assume that something is just weird in the test-suite (why would + -- this fail only here specifically (IIRC there are enough tests that + -- do something similar)), and since it's fine on 0.9 and master (which + -- matter much more) there shouldn't be an issue in practice. + exec_lua[[ + if require("luasnip.util.vimversion").ge(0,8,0) then + ls.jump(1) + end + ]] end) it("Deleting nested snippet only removes it.", function() feed("ofn") From 41b8811ddf0a6de49939b8037663e041221f8e97 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 12:19:41 +0100 Subject: [PATCH 28/69] snippetstring: store strings as \n-separated string. This should simplify applying string-operations. Also don't store the string of a snippet, just reconstruct it when needed. We do this because it is much easier than figuring out exactly how indent or expand affect a snippet (indentSnippetNode can affect them, so keeping them in sync manually seems infeasible.) --- lua/luasnip/nodes/insertNode.lua | 2 +- lua/luasnip/nodes/util.lua | 2 +- lua/luasnip/nodes/util/snippet_string.lua | 50 +++++++++++++++-------- lua/luasnip/util/util.lua | 14 ++++++- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 2b2851e36..ffd77f3c8 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -414,7 +414,7 @@ function InsertNode:get_static_text() if not self.visible and not self.static_visible then return nil end - return self.static_text:str() + return vim.split(self.static_text:str(), "\n") end function InsertNode:set_text(text) diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 86465f962..dd30020bc 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -858,7 +858,7 @@ end local function str_args(args) return args and vim.tbl_map(function(arg) - return snippet_string.isinstance(arg) and arg:str() or arg + return snippet_string.isinstance(arg) and vim.split(arg:str(), "\n") or arg end, args) end diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 3f273d322..0a210ebbd 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -5,7 +5,6 @@ local util = require("luasnip.util.util") local SnippetString = {} local SnippetString_mt = { __index = SnippetString, - __tostring = SnippetString.str } local M = {} @@ -14,7 +13,7 @@ local M = {} ---@param initial_str string[]?, optional initial multiline string. ---@return SnippetString function M.new(initial_str) - local o = {initial_str} + local o = {initial_str and table.concat(initial_str, "\n")} return setmetatable(o, SnippetString_mt) end @@ -22,38 +21,57 @@ function M.isinstance(o) return getmetatable(o) == SnippetString_mt end -function SnippetString:append_snip(snip, str) - table.insert(self, {snip = snip, str = str}) +function SnippetString:append_snip(snip) + table.insert(self, {snip = snip}) end function SnippetString:append_text(str) - table.insert(self, str) + table.insert(self, table.concat(str, "\n")) end + function SnippetString:str() - local str = {""} + local str = "" for _, snipstr_or_str in ipairs(self) do - str_util.multiline_append(str, snipstr_or_str.str and snipstr_or_str.str or snipstr_or_str) + if snipstr_or_str.snip then + snipstr_or_str.snip:subtree_do({ + pre = function(node) + if node.static_text then + if M.isinstance(node.static_text) then + str = str .. node.static_text:str() + else + str = str .. table.concat(node.static_text, "\n") + end + end + end, + post = util.nop + }) + else + str = str .. snipstr_or_str + end end return str end +SnippetString_mt.__tostring = SnippetString.str function SnippetString:indent(indentstr) - for _, snipstr_or_str in ipairs(self) do + for k, snipstr_or_str in ipairs(self) do if snipstr_or_str.snip then snipstr_or_str.snip:indent(indentstr) - util.indent(snipstr_or_str.str, indentstr) else - util.indent(snipstr_or_str, indentstr) + local str_tmp = vim.split(snipstr_or_str, "\n") + util.indent(str_tmp, indentstr) + self[k] = table.concat(str_tmp, "\n") end end end function SnippetString:expand_tabs(tabwidth, indenstrlen) - for _, snipstr_or_str in ipairs(self) do + for k, snipstr_or_str in ipairs(self) do if snipstr_or_str.snip then snipstr_or_str.snip:expand_tabs(tabwidth, indenstrlen) - util.expand_tabs(snipstr_or_str.str, tabwidth, indenstrlen) else - util.expand_tabs(snipstr_or_str, tabwidth, indenstrlen) + local str_tmp = vim.split(snipstr_or_str, "\n") + util.expand_tabs(str_tmp, tabwidth, indenstrlen) + self[k] = table.concat(str_tmp, "\n") end end end @@ -77,7 +95,7 @@ function SnippetString:put(pos) if snipstr_or_str.snip then snipstr_or_str.snip:put(pos) else - util.put(snipstr_or_str, pos) + util.put(vim.split(snipstr_or_str, "\n"), pos) end end end @@ -119,9 +137,9 @@ function SnippetString:reown(new_parent) -- set correct parent_node. snipcop.parent_node = new_parent - return {snip = snipcop, str = vim.deepcopy(snipstr_or_str.str)} + return {snip = snipcop} else - return vim.deepcopy(snipstr_or_str) + return snipstr_or_str end end, self), SnippetString_mt) end diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 8e71f6f5f..4ef07c9bb 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -406,6 +406,17 @@ local function pos_offset(base_pos, pos) return {row_offset, row_offset == 0 and pos[2] - base_pos[2] or pos[2]} end +local function shallow_copy(t) + if type(t) == "table" then + local res = {} + for k, v in pairs(t) do + res[k] = v + end + return res + end + return t +end + return { get_cursor_0ind = get_cursor_0ind, set_cursor_0ind = set_cursor_0ind, @@ -447,5 +458,6 @@ return { indx_of = indx_of, ternary = ternary, pos_cmp = pos_cmp, - pos_offset = pos_offset + pos_offset = pos_offset, + shallow_copy = shallow_copy } From 81a1c9d103e72cb95d28faf14e17c056f55e2546 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:18:44 +0100 Subject: [PATCH 29/69] small refactor. set parent_node in insert_into_jumplist, and rename reown to copy (so it can be used more generally and not just in the context of some insertNode receiving the snippetString as static_text). --- lua/luasnip/nodes/insertNode.lua | 3 ++- lua/luasnip/nodes/snippet.lua | 7 ++++--- lua/luasnip/nodes/util/snippet_string.lua | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index ffd77f3c8..00252a293 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -42,7 +42,7 @@ local function I(pos, static_text, opts) -- make static text owned by this insertNode. -- This includes copying it so that it is separate from the snippets that -- were potentially captured in `get_args`. - node.static_text = static_text:reown(node) + node.static_text = static_text:copy() return node end extend_decorator.register(I, { arg_indx = 3 }) @@ -406,6 +406,7 @@ function InsertNode:put_initial(pos) -- don't have to pass a current_node, we don't need it since we can -- certainly link the snippet into this insertNode. snip:insert_into_jumplist(nil, self, self.parent.snippet.child_snippets, child_snippet_idx) + child_snippet_idx = child_snippet_idx + 1 end end diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 706caad5a..34b80676e 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -531,6 +531,10 @@ function Snippet:insert_into_jumplist( -- have not yet inserted self!! local next_snippet = sibling_snippets[own_indx] + -- can set this immediately + -- parent_node is nil if the snippet is toplevel. + self.parent_node = parent_node + -- only consider sibling-snippets with the same parent-node as -- previous/next snippet for linking-purposes. -- They are siblings because they are expanded in the same snippet, not @@ -765,9 +769,6 @@ function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) self.insert_nodes[0].prev = self self.next = self.insert_nodes[0] - -- parent_node is nil if the snippet is toplevel. - self.parent_node = parent_node - self:put(pos) self:update() diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 0a210ebbd..dd7e35b3f 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -100,7 +100,7 @@ function SnippetString:put(pos) end end -function SnippetString:reown(new_parent) +function SnippetString:copy() -- on 0.7 vim.deepcopy does not behave correctly => have to manually copy. return setmetatable(vim.tbl_map(function(snipstr_or_str) if snipstr_or_str.snip then @@ -133,9 +133,9 @@ function SnippetString:reown(new_parent) post = util.nop, do_child_snippets = true }) + -- snippet may have been active (for example if captured as an + -- argnode), so finally exit here (so we can put_initial it again!) snipcop:exit() - -- set correct parent_node. - snipcop.parent_node = new_parent return {snip = snipcop} else From 9d0dfd64b58f5a7d3c166835b4aaec016b529f0b Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:20:40 +0100 Subject: [PATCH 30/69] implement a few simple string-operations on snippetString. lower, upper, and .. --- lua/luasnip/nodes/util/snippet_string.lua | 83 +++++++++++++++++++++++ lua/luasnip/util/str.lua | 11 +++ 2 files changed, 94 insertions(+) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index dd7e35b3f..eb4c84154 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -144,4 +144,87 @@ function SnippetString:copy() end, self), SnippetString_mt) end +-- copy without copying snippets. +function SnippetString:flatcopy() + local res = {} + for i, v in ipairs(self) do + res[i] = util.shallow_copy(v) + end + return setmetatable(res, SnippetString_mt) +end + +-- where o is string, string[] or SnippetString. +local function to_snippetstring(o) + if type(o) == "string" then + return M.new({o}) + elseif getmetatable(o) == SnippetString_mt then + return o + else + return M.new(o) + end +end + +function SnippetString.concat(a, b) + a = to_snippetstring(a):flatcopy() + b = to_snippetstring(b):flatcopy() + vim.list_extend(a, b) + + return a +end +SnippetString_mt.__concat = SnippetString.concat + +function SnippetString:_upper() + for i, v in ipairs(self) do + if v.snip then + v.snip:subtree_do({ + pre = function(node) + if node.static_text then + if M.isinstance(node.static_text) then + node.static_text:_upper() + else + str_util.multiline_upper(node.static_text) + end + end + end, + post = util.nop + }) + else + self[i] = v:upper() + end + end +end + +function SnippetString:upper() + local cop = self:copy() + cop:_upper() + return cop +end + +function SnippetString:_lower() + for i, v in ipairs(self) do + if v.snip then + v.snip:subtree_do({ + pre = function(node) + if node.static_text then + if M.isinstance(node.static_text) then + node.static_text:_lower() + else + str_util.multiline_lower(node.static_text) + end + end + end, + post = util.nop + }) + else + self[i] = v:lower() + end + end +end + +function SnippetString:lower() + local cop = self:copy() + cop:_lower() + return cop +end + return M diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index ef55daa44..628da7a7d 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -135,6 +135,17 @@ function M.multiline_substr(str, from, to) return res end +function M.multiline_upper(str) + for i, s in ipairs(str) do + str[i] = s:upper() + end +end +function M.multiline_lower(str) + for i, s in ipairs(str) do + str[i] = s:lower() + end +end + -- modifies strmod function M.multiline_append(strmod, strappend) strmod[#strmod] = strmod[#strmod] .. strappend[1] From 4f50d6c0bfb6af58ad5679418e538efdfa67e2d6 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:21:35 +0100 Subject: [PATCH 31/69] fix flakiness in test. --- tests/integration/snippet_basics_spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index eee9fc748..1eab1968d 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -1432,6 +1432,7 @@ describe("snippets_basic", function() } ]]) exec_lua([[ls.lsp_expand("a$1$1a")]]) + exec_lua("vim.wait(10, function() end)") exec_lua([[ls.lsp_expand("b$1")]]) feed("ccc") exec_lua([[ls.active_update_dependents()]]) From 78419f1369a1c71fc6765cd70446f9a38e02c5c4 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 15:55:50 +0100 Subject: [PATCH 32/69] update: try to find new active node in child-snippet. --- lua/luasnip/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index ecfcf065c..e7bf55530 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -170,7 +170,7 @@ local function get_corresponding_node(parent, data) return parent:find_node(function(test_node) return (test_node.store_id == data.store_id) or (data.key ~= nil and test_node.key == data.key) - end) + end, {find_in_child_snippets = true}) end local function restore_cursor_pos_relative(node, data) From 0213852ab229647c1ea9a58bf3b1f2882031410a Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 13:27:09 +0100 Subject: [PATCH 33/69] allow replacing parts of a snippetString with other text. If possible, snippets are preserved, and if they can't be preserved they will gracefully degrade to raw text. --- lua/luasnip/nodes/util/snippet_string.lua | 179 +++++++++++++++++++++- 1 file changed, 172 insertions(+), 7 deletions(-) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index eb4c84154..e59f2d666 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -28,28 +28,47 @@ function SnippetString:append_text(str) table.insert(self, table.concat(str, "\n")) end -function SnippetString:str() +-- compute table mapping +-- * each snippet in this snipstr (including nested) to its string-content +-- * each component in the snippet_string (including nested) to the text-index +-- of its first character. +-- * the string of each nested snippetString. +local function gen_snipstr_map(self, map, from_offset) + map[self] = {} + local str = "" - for _, snipstr_or_str in ipairs(self) do - if snipstr_or_str.snip then - snipstr_or_str.snip:subtree_do({ + for i, v in ipairs(self) do + map[self][i] = from_offset + #str + if v.snip then + local snip_str = "" + v.snip:subtree_do({ pre = function(node) if node.static_text then if M.isinstance(node.static_text) then - str = str .. node.static_text:str() + local nested_str = gen_snipstr_map(node.static_text, map, from_offset + #str + #snip_str) + snip_str = snip_str .. nested_str else - str = str .. table.concat(node.static_text, "\n") + snip_str = snip_str .. table.concat(node.static_text, "\n") end end end, post = util.nop }) + map[v.snip] = snip_str + str = str .. snip_str else - str = str .. snipstr_or_str + str = str .. v end end + map[self].str = str return str end + +function SnippetString:str() + -- if too slow, generate another version of that function without the + -- snipstr_map-calls. + return gen_snipstr_map(self, {}, 1) +end SnippetString_mt.__tostring = SnippetString.str function SnippetString:indent(indentstr) @@ -173,6 +192,152 @@ function SnippetString.concat(a, b) end SnippetString_mt.__concat = SnippetString.concat +-- for generic string-operations: we can apply them _and_ keep the snippet as +-- long as a change to the string does not span over extmarks! We need to verify +-- this somehow, and can do this by storing the positions where one extmark ends +-- and another begins in some list or table which is quickly queried. +-- Since all string-operations work with simple strings and not the +-- string-tables we have here usually, we should also convert {"a", "b"} to +-- "a\nb". This also simplifies storing the positions where some node ends, and +-- is much better than converting all the time when a string-operation is +-- involved. + +-- only call after it's clear that char_i is contained in self. +local function find(self, start_i, i_inc, char_i, snipstr_map) + local i = start_i + while true do + local v = self[i] + local current_str_from = snipstr_map[self][i] + if not v then + -- leave in for now, no endless loops while testing :D + error("huh??") + end + local v_str + if v.snip then + v_str = snipstr_map[v.snip] + else + v_str = v + end + + local current_str_to = current_str_from + #v_str-1 + if char_i >= current_str_from and char_i <= current_str_to then + return i + end + + i = i + i_inc + end +end + +local function nodetext_len(node, snipstr_map) + if not node.static_text then + return 0 + end + + if M.isinstance(node.static_text) then + return #snipstr_map[node.static_text].str + else + -- +1 for each newline. + local len = #node.static_text-1 + for _, v in ipairs(node.static_text) do + len = len + #v + end + return len + end +end + +-- replacements may not be zero-width! +local function _replace(self, replacements, snipstr_map) + -- first character of currently-looked-at text. + local v_i_search_from = #self + + for i = #replacements, 1, -1 do + local repl = replacements[i] + + local v_i_to = find(self, v_i_search_from, -1 , repl.from, snipstr_map) + local v_i_from = find(self, v_i_to, -1, repl.to, snipstr_map) + + -- next range may begin in v_i_from, before the currently inserted + -- one. + v_i_search_from = v_i_from + + -- first characters of v_from and v_to respectively. + local v_from_from = snipstr_map[self][v_i_from] + local v_to_from = snipstr_map[self][v_i_to] + local _, repl_in_node = nil, false + + if v_i_from == v_i_to and self[v_i_from].snip then + local snip = self[v_i_from].snip + local node_from = v_from_from + + -- will probably always error, res is true if the substitution + -- could be done, false if repl spans multiple nodes. + _, repl_in_node = pcall(snip.subtree_do, snip, { + pre = function(node) + local node_len = nodetext_len(node, snipstr_map) + if node_len > 0 then + local node_relative_repl_from = repl.from - node_from+1 + local node_relative_repl_to = repl.to - node_from+1 + + if node_relative_repl_from >= 1 and node_relative_repl_from <= node_len then + if node_relative_repl_to <= node_len then + if M.isinstance(node.static_text) then + -- node contains a snippetString, recurse! + -- since we only check string-positions via + -- snipstr_map, we don't even have to + -- modify repl to be defined based on the + -- other snippetString. (ie. shift from and to) + _replace(node.static_text, {repl}, snipstr_map) + else + -- simply manipulate the node-static-text + -- manually. + -- + -- we don't need to update the snipstr_map + -- because even if this same node or same + -- snippet contains another range (which is + -- the only data in snipstr_map we may + -- access that is inaccurate), the queries + -- will still be answered correctly. + local str = table.concat(node.static_text, "\n") + node.static_text = vim.split( + str:sub(1, node_relative_repl_from-2) .. repl.str .. str:sub(node_relative_repl_to+1), "\n") + end + -- update string in snipstr_map. + snipstr_map[snip] = snipstr_map[snip]:sub(1, repl.from - v_from_from-1) .. repl.str .. snipstr_map[snip]:sub(repl.to - v_to_from+1) + error(true) + else + -- range begins in, but ends outside this node + -- => snippet cannot be preserved. + -- Replace it with its static text and do the + -- replacement on that. + error(false) + end + end + node_from = node_from + node_len + end + end, + post = util.nop + }) + end + -- in lieu of `continue`, we need this bool to check whether we did a replacement yet. + if not repl_in_node then + local from_str = self[v_i_from].snip and snipstr_map[self[v_i_from].snip] or self[v_i_from] + local to_str = self[v_i_to].snip and snipstr_map[self[v_i_to].snip] or self[v_i_to] + + -- +1 to get the char of to, +1 to start beyond it. + self[v_i_from] = from_str:sub(1, repl.from - v_from_from) .. repl.str .. to_str:sub(repl.to - v_to_from+1+1) + -- start-position of string has to be updated. + snipstr_map[self][v_i_from] = v_from_from + end + end +end + +-- replacements may not be zero-width! +function SnippetString:replace(replacements) + local snipstr_map = {} + gen_snipstr_map(self, snipstr_map, 1) + _replace(self, replacements, snipstr_map) +end + function SnippetString:_upper() for i, v in ipairs(self) do if v.snip then From 83a2793be861d201e43f77fe1be079021e736920 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 13:34:24 +0100 Subject: [PATCH 34/69] implement gsub on snippetString. --- lua/luasnip/nodes/util/snippet_string.lua | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index e59f2d666..4cad5646e 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -338,6 +338,38 @@ function SnippetString:replace(replacements) _replace(self, replacements, snipstr_map) end +-- gsub will preserve snippets as long as a substituted region does not overlap +-- more than one node. +-- gsub will ignore zero-length matches. In these cases, it becomes less easy +-- to define the association of new string -> static_text it should be +-- associated with, so these are ignored (until a sensible behaviour is clear +-- (maybe respect rgrav behaviour? does not seem useful)). +function SnippetString:gsub(pattern, repl) + self = self:copy() + + local find_from = 1 + local str = self:str() + local replacements = {} + while true do + local match_from, match_to = str:find(pattern, find_from) + if not match_from then + break + end + -- only allow matches that are not empty. + if match_from ~= match_to then + table.insert(replacements, { + from = match_from, + to = match_to, + str = str:sub(match_from, match_to):gsub(pattern, repl) + }) + end + find_from = match_to + 1 + end + self:replace(replacements) + + return self +end + function SnippetString:_upper() for i, v in ipairs(self) do if v.snip then From b14f25f24a6a5458140b504eda65f1f18b71ffbc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 18:29:57 +0100 Subject: [PATCH 35/69] snippetstring.replace: fix substitution in textNode. --- lua/luasnip/nodes/util/snippet_string.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 4cad5646e..9481792af 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -299,7 +299,7 @@ local function _replace(self, replacements, snipstr_map) -- will still be answered correctly. local str = table.concat(node.static_text, "\n") node.static_text = vim.split( - str:sub(1, node_relative_repl_from-2) .. repl.str .. str:sub(node_relative_repl_to+1), "\n") + str:sub(1, node_relative_repl_from-1) .. repl.str .. str:sub(node_relative_repl_to+1), "\n") end -- update string in snipstr_map. snipstr_map[snip] = snipstr_map[snip]:sub(1, repl.from - v_from_from-1) .. repl.str .. snipstr_map[snip]:sub(repl.to - v_to_from+1) From bd58f4dcb29ee69aa2bfddb9853bd869297b2a41 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 21:08:27 +0100 Subject: [PATCH 36/69] fix switchup. --- lua/luasnip/nodes/util/snippet_string.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 9481792af..1f4d49fc0 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -253,8 +253,8 @@ local function _replace(self, replacements, snipstr_map) for i = #replacements, 1, -1 do local repl = replacements[i] - local v_i_to = find(self, v_i_search_from, -1 , repl.from, snipstr_map) - local v_i_from = find(self, v_i_to, -1, repl.to, snipstr_map) + local v_i_to = find(self, v_i_search_from, -1 , repl.to, snipstr_map) + local v_i_from = find(self, v_i_to, -1, repl.from, snipstr_map) -- next range may begin in v_i_from, before the currently inserted -- one. From 56b9e1a30aad869ebd586dcaa6db059341305992 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 22:14:51 +0100 Subject: [PATCH 37/69] make in-place modifying functions private. --- lua/luasnip/nodes/util/snippet_string.lua | 86 ++++++++++++----------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 1f4d49fc0..6949c1944 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -332,45 +332,13 @@ local function _replace(self, replacements, snipstr_map) end -- replacements may not be zero-width! -function SnippetString:replace(replacements) +local function replace(self, replacements) local snipstr_map = {} gen_snipstr_map(self, snipstr_map, 1) _replace(self, replacements, snipstr_map) end --- gsub will preserve snippets as long as a substituted region does not overlap --- more than one node. --- gsub will ignore zero-length matches. In these cases, it becomes less easy --- to define the association of new string -> static_text it should be --- associated with, so these are ignored (until a sensible behaviour is clear --- (maybe respect rgrav behaviour? does not seem useful)). -function SnippetString:gsub(pattern, repl) - self = self:copy() - - local find_from = 1 - local str = self:str() - local replacements = {} - while true do - local match_from, match_to = str:find(pattern, find_from) - if not match_from then - break - end - -- only allow matches that are not empty. - if match_from ~= match_to then - table.insert(replacements, { - from = match_from, - to = match_to, - str = str:sub(match_from, match_to):gsub(pattern, repl) - }) - end - find_from = match_to + 1 - end - self:replace(replacements) - - return self -end - -function SnippetString:_upper() +local function upper(self) for i, v in ipairs(self) do if v.snip then v.snip:subtree_do({ @@ -391,13 +359,7 @@ function SnippetString:_upper() end end -function SnippetString:upper() - local cop = self:copy() - cop:_upper() - return cop -end - -function SnippetString:_lower() +local function lower(self) for i, v in ipairs(self) do if v.snip then v.snip:subtree_do({ @@ -420,8 +382,48 @@ end function SnippetString:lower() local cop = self:copy() - cop:_lower() + lower(cop) return cop end +function SnippetString:upper() + local cop = self:copy() + upper(cop) + return cop +end + +-- gsub will preserve snippets as long as a substituted region does not overlap +-- more than one node. +-- gsub will ignore zero-length matches. In these cases, it becomes less easy +-- to define the association of new string -> static_text it should be +-- associated with, so these are ignored (until a sensible behaviour is clear +-- (maybe respect rgrav behaviour? does not seem useful)). +-- Also, it should be straightforward to circumvent this by doing something +-- like :gsub("(.)", "%1_") or :gsub("(.)", "_%1") to choose the "side" where a +-- new char is inserted, +function SnippetString:gsub(pattern, repl) + self = self:copy() + + local find_from = 1 + local str = self:str() + local replacements = {} + while true do + local match_from, match_to = str:find(pattern, find_from) + if not match_from then + break + end + -- only allow matches that are not empty. + if match_from <= match_to then + table.insert(replacements, { + from = match_from, + to = match_to, + str = str:sub(match_from, match_to):gsub(pattern, repl) + }) + end + find_from = match_to + 1 + end + replace(self, replacements) + + return self +end return M From 483656e8571db7a85f215e27c30196be07a29884 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 22:15:18 +0100 Subject: [PATCH 38/69] add :sub to snippetString. --- lua/luasnip/nodes/util/snippet_string.lua | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 6949c1944..bba59c7cf 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -426,4 +426,43 @@ function SnippetString:gsub(pattern, repl) return self end +function SnippetString:sub(from, to) + self = self:copy() + + local snipstr_map = {} + local str = gen_snipstr_map(self, snipstr_map, 1) + + to = to or #str + + -- negative -> positive + if from < 0 then + from = #str + from + 1 + end + if to < 0 then + to = #str + to + 1 + end + + -- empty range => return empty snippetString. + if from > #str or to < from or to < 1 then + return M.new({""}) + end + + from = math.max(from, 1) + to = math.min(to, #str) + + local replacements = {} + -- from <= 1 => don't need to remove from beginning. + if from > 1 then + table.insert(replacements, { from=1, to=from-1, str = "" }) + end + -- to >= #str => don't need to remove from end. + if to < #str then + table.insert(replacements, { from=to+1, to=#str, str = "" }) + end + + _replace(self, replacements, snipstr_map) + return self +end + + return M From 1d8aa6523470176e44f5c3105a48e071698d393d Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 3 Nov 2024 10:22:13 +0100 Subject: [PATCH 39/69] add `opt` for the optional argument. --- lua/luasnip/default_config.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/luasnip/default_config.lua b/lua/luasnip/default_config.lua index 9793cf5c8..3bce73353 100644 --- a/lua/luasnip/default_config.lua +++ b/lua/luasnip/default_config.lua @@ -51,6 +51,9 @@ local lazy_snip_env = { k = function() return require("luasnip.nodes.key_indexer").new_key end, + opt = function() + return require("luasnip.nodes.optional_arg").new_opt + end, ai = function() return require("luasnip.nodes.absolute_indexer") end, From de53515d7698af1a1ebcf8dfa1176ee84bbd0e43 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 3 Nov 2024 16:10:02 +0100 Subject: [PATCH 40/69] correctly store+restore visual selection during update. --- lua/luasnip/init.lua | 20 +++++++++-- tests/integration/dynamic_spec.lua | 55 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index e7bf55530..5748e0c37 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -2,6 +2,7 @@ local util = require("luasnip.util.util") local lazy_table = require("luasnip.util.lazy_table") local types = require("luasnip.util.types") local node_util = require("luasnip.nodes.util") +local feedkeys = require("luasnip.util.feedkeys") local session = require("luasnip.session") local snippet_collection = require("luasnip.session.snippet_collection") @@ -158,6 +159,12 @@ local function store_cursor_node_relative(node) snip_data.cursor_end_relative = util.pos_sub(util.get_cursor_0ind(), node.mark:get_endpoint(1)) + if vim.fn.mode() == "s" then + local getpos_v = vim.fn.getpos("v") + local selection_end_pos = {getpos_v[2]-1, getpos_v[3]} + snip_data.selection_other_end_end_relative = util.pos_sub(selection_end_pos, node.mark:get_endpoint(1)) + end + data[snip] = snip_data snippet_current_node = snip:get_snippet().parent_node @@ -174,9 +181,16 @@ local function get_corresponding_node(parent, data) end local function restore_cursor_pos_relative(node, data) - util.set_cursor_0ind( - util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) - ) + if data.selection_other_end_end_relative then + -- is a selection => restore it. + local selection_from = util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) + local selection_to = util.pos_add(node.mark:get_endpoint(1), data.selection_other_end_end_relative) + feedkeys.select_range(selection_from, selection_to) + else + util.set_cursor_0ind( + util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) + ) + end end local function node_update_dependents_preserve_position(node, opts) diff --git a/tests/integration/dynamic_spec.lua b/tests/integration/dynamic_spec.lua index 655dedbbf..45d4125a6 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -367,4 +367,59 @@ describe("DynamicNode", function() ) end ) + + it("dynamicNode can depend on itself.", function() + exec_lua([[ + ls.setup({ + update_events = "TextChangedI" + }) + ls.snip_expand(s("trig", { + d(1, function(args) + if not args[1] then + return sn(nil, {i(1, "asdf", {key = "ins"})}) + else + return sn(nil, {i(1, args[1][1]:gsub("a", "e"), {key = "ins"})}) + end + end, {opt(k("ins"))}) + })) + ]]) +screen:expect({ + grid = [[ + ^e{3:sdf} | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + feed("aaaaa") +screen:expect({ + grid = [[ + eeeee^ | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + end) + + it("selected text is selected again after updating (when possible).", function() + exec_lua[[ + ls.snip_expand(s("trig", { + d(1, function(args) + if not args[1] then + return sn(nil, {i(1, "asdf", {key = "ins"})}) + else + return sn(nil, {i(1, args[1]:gsub("a", "e"), {key = "ins"})}) + end + end, {opt(k("ins"))}, {snippetstring_args = true}) + })) + ]] + feed("a") + exec_lua("ls.lsp_expand('${1:asdf}')") +screen:expect({ + grid = [[ + e^e{3:sdf}sdf | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + end) end) From 31f974072e305f24b55fe7fa985c84c9993bb0fc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:17:49 +0100 Subject: [PATCH 41/69] fNode: always store result in static_text. :store only really makes sense for nodes where we can't store the text as its' being generated, which is exclusively insertNode. --- lua/luasnip/nodes/functionNode.lua | 1 + lua/luasnip/nodes/node.lua | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index 0fec9c258..f9224712c 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -58,6 +58,7 @@ function FunctionNode:update() -- don't expand tabs in parent.indentstr, use it as-is. self:set_text_raw(util.indent(text, self.parent.indentstr)) + self.static_text = text -- assume that functionNode can't have a parent as its dependent, there is -- no use for that I think. diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index e37055c89..25bf7d8da 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -330,10 +330,8 @@ function Node:set_ext_opts(name) end end --- for insert,functionNode. -function Node:store() - self.static_text = self:get_text() -end +-- default impl. for textNode and functionNode (fNode stores after an update). +function Node:store() end function Node:update_restore() end From 4244a886b11b1ccac5603f518c12b211f8c4c848 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:42:07 +0100 Subject: [PATCH 42/69] update_dependents: get cursor-position after queried movements. --- lua/luasnip/init.lua | 9 +++--- lua/luasnip/util/feedkeys.lua | 55 +++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 5748e0c37..7156207f0 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -156,13 +156,12 @@ local function store_cursor_node_relative(node) store_id = store_id + 1 + local cursor_state = feedkeys.last_state() snip_data.cursor_end_relative = - util.pos_sub(util.get_cursor_0ind(), node.mark:get_endpoint(1)) + util.pos_sub(cursor_state.pos, node.mark:get_endpoint(1)) - if vim.fn.mode() == "s" then - local getpos_v = vim.fn.getpos("v") - local selection_end_pos = {getpos_v[2]-1, getpos_v[3]} - snip_data.selection_other_end_end_relative = util.pos_sub(selection_end_pos, node.mark:get_endpoint(1)) + if cursor_state.pos_end then + snip_data.selection_other_end_end_relative = util.pos_sub(cursor_state.pos_end, node.mark:get_endpoint(1)) end data[snip] = snip_data diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 7181eff42..130fb49aa 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -23,6 +23,7 @@ local executing_id = nil -- contains functions which take exactly one argument, the id. local enqueued_actions = {} +local enqueued_cursor_state local function _feedkeys_insert(id, keys) executing_id = id @@ -43,11 +44,11 @@ local function _feedkeys_insert(id, keys) ) end -local function enqueue_action(fn) - -- get unique id and increment global. - local keys_id = current_id +local function next_id() current_id = current_id + 1 - + return current_id - 1 +end +local function enqueue_action(fn, keys_id) -- if there is nothing from luasnip currently executing, we may just insert -- into the typeahead if executing_id == nil then @@ -60,7 +61,7 @@ end function M.feedkeys_insert(keys) enqueue_action(function(id) _feedkeys_insert(id, keys) - end) + end, next_id()) end -- pos: (0,0)-indexed. @@ -88,7 +89,9 @@ local function cursor_set_keys(pos, before) end function M.select_range(b, e) - enqueue_action(function(id) + local id = next_id() + enqueued_cursor_state = {pos = vim.deepcopy(b), pos_end = vim.deepcopy(e), id = id} + enqueue_action(function() -- stylua: ignore _feedkeys_insert(id, -- this esc -> movement sometimes leads to a slight flicker @@ -114,12 +117,15 @@ function M.select_range(b, e) -- set before cursor_set_keys(e, true)) .. "o_" ) - end) + end, id) end -- move the cursor to a position and enter insert-mode (or stay in it). function M.insert_at(pos) - enqueue_action(function(id) + local id = next_id() + enqueued_cursor_state = {pos = pos, id = id} + + enqueue_action(function() -- if current and target mode is INSERT, there's no reason to leave it. if vim.fn.mode() == "i" then -- can skip feedkeys here, we can complete this command from lua. @@ -133,16 +139,47 @@ function M.insert_at(pos) -- mode might be VISUAL or something else => to know we're in NORMAL. _feedkeys_insert(id, "i" .. cursor_set_keys(pos)) end - end) + end, id) end function M.confirm(id) executing_id = nil + if enqueued_cursor_state and enqueued_cursor_state.id == id then + -- only clear state if set by this action. + enqueued_cursor_state = nil + end + if enqueued_actions[id + 1] then enqueued_actions[id + 1](id + 1) enqueued_actions[id + 1] = nil end end +-- if there are some operations that move the cursor enqueud, retrieve their +-- target-state, otherwise return the current cursor state. +function M.last_state() + if enqueued_cursor_state then + local state = vim.deepcopy(enqueued_cursor_state) + state.id = nil + return state + end + + local state = {} + + local getposdot = vim.fn.getpos(".") + state.pos = {getposdot[2]-1, getposdot[3]-1} + + -- only re-enter select for now. + if vim.fn.mode() == "s" then + local getposv = vim.fn.getpos("v") + -- store selection-range with end-position one column after the cursor + -- at the end (so -1 to make getpos-position 0-based, +1 to move it one + -- beyond the last character of the range) + state.pos_end = {getposv[2]-1, getposv[3]+1} + end + + return state +end + return M From dfe82fba77970ab25c3c918b61cf9033463c3ff0 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:43:09 +0100 Subject: [PATCH 43/69] move the jump_active-check into the autocommand. Whenever update_dependents is called by luasnip, we can be sure that it's safe to call currently. --- lua/luasnip/config.lua | 9 ++++++++- lua/luasnip/init.lua | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lua/luasnip/config.lua b/lua/luasnip/config.lua index c650d1aac..41518b050 100644 --- a/lua/luasnip/config.lua +++ b/lua/luasnip/config.lua @@ -103,7 +103,14 @@ c = { end ls_autocmd( session.config.update_events, - require("luasnip").active_update_dependents + function() + -- don't update due to events if an update due to luasnip is pending anyway. + -- (Also, this would be bad because luasnip may not be in an + -- consistent state whenever an autocommand is triggered) + if not session.jump_active then + require("luasnip").active_update_dependents() + end + end ) if session.config.region_check_events ~= nil then ls_autocmd(session.config.region_check_events, function() diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 7156207f0..eff5eb46d 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -282,7 +282,7 @@ local function active_update_dependents() local active = session.current_nodes[vim.api.nvim_get_current_buf()] -- don't update if a jump/change_choice is in progress, or if we don't have -- an active node. - if not session.jump_active and active ~= nil then + if active ~= nil then local upd_res = node_update_dependents_preserve_position( active, { no_move = false, restore_position = true } @@ -479,7 +479,7 @@ local function snip_expand(snippet, opts) -- schedule update of active node. -- Not really happy with this, but for some reason I don't have time to -- investigate, nvim_buf_get_text does not return the updated text :/ - vim.schedule(active_update_dependents) + active_update_dependents() return snip end From be863e7ca812a11913fba3d973966472d2eff49e Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:44:30 +0100 Subject: [PATCH 44/69] optionally update a node differnt from the current node. --- lua/luasnip/init.lua | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index eff5eb46d..35d39de9d 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -192,8 +192,8 @@ local function restore_cursor_pos_relative(node, data) end end -local function node_update_dependents_preserve_position(node, opts) - local restore_data = store_cursor_node_relative(node) +local function node_update_dependents_preserve_position(node, current, opts) + local restore_data = store_cursor_node_relative(current) -- update all nodes that depend on this one. local ok, res = @@ -209,19 +209,19 @@ local function node_update_dependents_preserve_position(node, opts) ) return { jump_done = false, - new_node = session.current_nodes[vim.api.nvim_get_current_buf()], + new_current = session.current_nodes[vim.api.nvim_get_current_buf()], } end -- update successful => check if the current node is still visible. - if node.visible then + if current.visible then if not opts.no_move and opts.restore_position then -- node is visible: restore position. - local active_snippet = node:get_snippet() - restore_cursor_pos_relative(node, restore_data[active_snippet]) + local active_snippet = current:get_snippet() + restore_cursor_pos_relative(current, restore_data[active_snippet]) end - return { jump_done = false, new_node = node } + return { jump_done = false, new_current = current } else -- node not visible => need to find a new node to set as current. @@ -231,7 +231,7 @@ local function node_update_dependents_preserve_position(node, opts) local parent_node = active_snippet.parent_node if not parent_node then -- very unlikely/not possible: all snippets are exited. - return { jump_done = false, new_node = nil } + return { jump_done = false, new_current = nil } end active_snippet = parent_node:get_snippet() end @@ -256,41 +256,45 @@ local function node_update_dependents_preserve_position(node, opts) "Visible dynamicNode that was a parent of the current node is not active after the update!! If you get this message, please open an issue with LuaSnip!" ) - local new_node = get_corresponding_node(d, snip_restore_data) + local new_current = get_corresponding_node(d, snip_restore_data) - if new_node then - node_util.refocus(d, new_node) + if new_current then + node_util.refocus(d, new_current) if not opts.no_move and opts.restore_position then -- node is visible: restore position - restore_cursor_pos_relative(new_node, snip_restore_data) + restore_cursor_pos_relative(new_current, snip_restore_data) end - return { jump_done = false, new_node = new_node } + return { jump_done = false, new_current = new_current } else -- could not find corresponding node -> just jump into the -- dynamicNode that should have generated it. return { jump_done = true, - new_node = d:jump_into_snippet(opts.no_move), + new_current = d:jump_into_snippet(opts.no_move), } end end end -local function active_update_dependents() +local function update_dependents(node) local active = session.current_nodes[vim.api.nvim_get_current_buf()] -- don't update if a jump/change_choice is in progress, or if we don't have -- an active node. if active ~= nil then local upd_res = node_update_dependents_preserve_position( + node, active, { no_move = false, restore_position = true } ) - upd_res.new_node:focus() - session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_node + upd_res.new_current:focus() + session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_current end end +local function active_update_dependents() + update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()]) +end -- return next active node. local function safe_jump_current(dir, no_move, dry_run) @@ -302,13 +306,14 @@ local function safe_jump_current(dir, no_move, dry_run) -- don't update for -1-node. if not dry_run and node.pos >= 0 then local upd_res = node_update_dependents_preserve_position( + node, node, { no_move = no_move, restore_position = false } ) if upd_res.jump_done then - return upd_res.new_node + return upd_res.new_current else - node = upd_res.new_node + node = upd_res.new_current end end From 2cb3fa191f026f48a283792a68f0ef6905b42a89 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:44:53 +0100 Subject: [PATCH 45/69] update_dependents: use update_restore by default. if we can restore a previously generated snippet, we should do so to not revert user input. --- lua/luasnip/nodes/node.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 25bf7d8da..f044c63f7 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -632,7 +632,7 @@ function Node:update_dependents(which) local dependents = node_util.collect_dependents(self, which, false) for _, node in ipairs(dependents) do if node.visible then - node:update() + node:update_restore() end end end From 9020719b059bc04a1b30db9ac58d4d22ff96c7ed Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:46:00 +0100 Subject: [PATCH 46/69] choiceNode: call update_dependents after routine is done completely. safer, update_dependents could remove the entire choiceNode, not good! --- lua/luasnip/init.lua | 2 ++ lua/luasnip/nodes/choiceNode.lua | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 35d39de9d..f2507baa0 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -622,6 +622,7 @@ local function change_choice(val) session.current_nodes[vim.api.nvim_get_current_buf()] ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active + active_update_dependents() end local function set_choice(choice_indx) @@ -639,6 +640,7 @@ local function set_choice(choice_indx) session.current_nodes[vim.api.nvim_get_current_buf()] ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active + active_update_dependents() end local function get_current_choices() diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index ccd4d28c7..68377f295 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -248,7 +248,7 @@ function ChoiceNode:set_choice(choice, current_node) -- -- active_choice has to be disabled (nilled?) to prevent reading from -- cleared mark in set_mark_rgrav (which will be called in - -- self:set_text({""}) a few lines below). + -- self:set_text_raw({""}) a few lines below). self.active_choice = nil self:set_text_raw({ "" }) @@ -271,10 +271,8 @@ function ChoiceNode:set_choice(choice, current_node) self.active_choice:subtree_set_pos_rgrav(to, -1, true) self.active_choice:update_restore() - self:update_dependents({ own = true, parents = true, children = true }) + -- update outside dependents later, in init.lua:set_choice! - -- Another node may have been entered in update_dependents. - self:focus() self:event(events.change_choice) if self.restore_cursor then From 1ee7430efb7a157904599bf57fada95af3588f42 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:54:03 +0100 Subject: [PATCH 47/69] move no_region_wrap back into main-module. --- lua/luasnip/init.lua | 21 +++++++++++++++++---- lua/luasnip/util/util.lua | 10 ---------- tests/integration/snippet_basics_spec.lua | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index f2507baa0..99ddf47f5 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -18,6 +18,18 @@ local luasnip_data_dir = vim.fn.stdpath("cache") .. "/luasnip" local log = require("luasnip.util.log").new("main") +local function no_region_check_wrap(fn, ...) + session.jump_active = true + -- will run on next tick, after autocommands (especially CursorMoved) for this are done. + vim.schedule(function() + session.jump_active = false + end) + + local fn_res = fn(...) + return fn_res +end + + local function get_active_snip() local node = session.current_nodes[vim.api.nvim_get_current_buf()] if not node then @@ -328,7 +340,7 @@ end local function jump(dir) local current = session.current_nodes[vim.api.nvim_get_current_buf()] if current then - local next_node = util.no_region_check_wrap(safe_jump_current, dir) + local next_node = no_region_check_wrap(safe_jump_current, dir) if next_node == nil then session.current_nodes[vim.api.nvim_get_current_buf()] = nil return true @@ -401,7 +413,7 @@ local function locally_jumpable(dir) end local function _jump_into_default(snippet) - return util.no_region_check_wrap(snippet.jump_into, snippet, 1) + return no_region_check_wrap(snippet.jump_into, snippet, 1) end -- opts.clear_region: table, keys `from` and `to`, both (0,0)-indexed. @@ -613,7 +625,7 @@ local function change_choice(val) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] assert(active_choice, "No active choiceNode") - local new_active = util.no_region_check_wrap( + local new_active = no_region_check_wrap( safe_choice_action, active_choice.parent.snippet, active_choice.change_choice, @@ -631,7 +643,7 @@ local function set_choice(choice_indx) assert(active_choice, "No active choiceNode") local choice = active_choice.choices[choice_indx] assert(choice, "Invalid Choice") - local new_active = util.no_region_check_wrap( + local new_active = no_region_check_wrap( safe_choice_action, active_choice.parent.snippet, active_choice.set_choice, @@ -1020,6 +1032,7 @@ ls = lazy_table({ extend_decorator = extend_decorator, log = require("luasnip.util.log"), activate_node = activate_node, + no_region_check_wrap = no_region_check_wrap }, ls_lazy) return ls diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 4ef07c9bb..b4a58ebcc 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -331,15 +331,6 @@ local function key_sorted_pairs(t) end end -local function no_region_check_wrap(fn, ...) - session.jump_active = true - -- will run on next tick, after autocommands (especially CursorMoved) for this are done. - vim.schedule(function() - session.jump_active = false - end) - return fn(...) -end - local function id(a) return a end @@ -449,7 +440,6 @@ return { deduplicate = deduplicate, pop_front = pop_front, key_sorted_pairs = key_sorted_pairs, - no_region_check_wrap = no_region_check_wrap, id = id, no = no, yes = yes, diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index 1eab1968d..260199a43 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -61,7 +61,7 @@ describe("snippets_basic", function() ls.expand({ jump_into_func = function(snip) izero = snip.insert_nodes[0] - require("luasnip.util.util").no_region_check_wrap(izero.jump_into, izero, 1) + require("luasnip").no_region_check_wrap(izero.jump_into, izero, 1) end }) ]]) From 155331f822a0004c52038f86355a3df9269db526 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:55:43 +0100 Subject: [PATCH 48/69] dynamicNode/restoreNode: don't destroy snip on exit. And store generated snippet in .snip, not .snip_stored, which is not reached by subtree_do, which we would like to have apply to the stored snippet. --- lua/luasnip/nodes/dynamicNode.lua | 19 +++++++++---------- lua/luasnip/nodes/restoreNode.lua | 5 ++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index f5efd4394..9834bf4c2 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -332,8 +332,6 @@ function DynamicNode:exit() if self.snip then self.snip:exit() end - self.stored_snip = self.snip - self.snip = nil self.active = false end @@ -341,7 +339,7 @@ function DynamicNode:set_ext_opts(name) Node.set_ext_opts(self, name) -- might not have been generated (missing nodes). - if self.snip then + if self.snip and self.snip.visible then self.snip:set_ext_opts(name) end end @@ -357,8 +355,9 @@ function DynamicNode:update_restore() local args = self:get_args() local str_args = node_util.str_args(args) - if self.stored_snip and vim.deep_equal(str_args, self.last_args) then - local tmp = self.stored_snip + -- only insert snip if it is not currently visible! + if self.snip and not self.snip.visible and vim.deep_equal(str_args, self.last_args) then + local tmp = self.snip -- position might (will probably!!) still have changed, so update it -- here too (as opposed to only in update). @@ -370,8 +369,8 @@ function DynamicNode:update_restore() tmp:set_dependents() tmp:set_argnodes(self.parent.snippet.dependents_dict) - -- sets own extmarks false,true - self:focus() + -- also focuses node, and sets own extmarks false,true + self:set_text_raw({ "" }) tmp.mark = self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts())) @@ -441,20 +440,20 @@ end function DynamicNode:subtree_set_pos_rgrav(pos, direction, rgrav) self.mark:set_rgrav(-direction, rgrav) - if self.snip then + if self.snip and self.snip.visible then self.snip:subtree_set_pos_rgrav(pos, direction, rgrav) end end function DynamicNode:subtree_set_rgrav(rgrav) self.mark:set_rgravs(rgrav, rgrav) - if self.snip then + if self.snip and self.snip.visible then self.snip:subtree_set_rgrav(rgrav) end end function DynamicNode:extmarks_valid() - if self.snip then + if self.snip and self.snip.visible then return node_util.generic_extmarks_valid(self, self.snip) end return true diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index 8519eb0d5..945445e27 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -40,7 +40,6 @@ function RestoreNode:exit() -- will be copied on restore, no need to copy here too. self.parent.snippet.stored[self.key] = self.snip self.snip:exit() - self.snip = nil self.active = false end @@ -273,14 +272,14 @@ end function RestoreNode:subtree_set_pos_rgrav(pos, direction, rgrav) self.mark:set_rgrav(-direction, rgrav) - if self.snip then + if self.snip and self.snip.visible then self.snip:subtree_set_pos_rgrav(pos, direction, rgrav) end end function RestoreNode:subtree_set_rgrav(rgrav) self.mark:set_rgravs(rgrav, rgrav) - if self.snip then + if self.snip and self.snip.visible then self.snip:subtree_set_rgrav(rgrav) end end From 9c70626ddcabda1c91c9ce45f0beced2960ce417 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:56:43 +0100 Subject: [PATCH 49/69] handle selection on first line and column of buffer with `before`. --- lua/luasnip/util/feedkeys.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 130fb49aa..b6d125eda 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -68,11 +68,13 @@ end local function cursor_set_keys(pos, before) if before then if pos[2] == 0 then - pos[1] = pos[1] - 1 - -- pos2 is set to last columnt of previous line. - -- # counts bytes, but win_set_cursor expects bytes, so all's good. - pos[2] = - #vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1] + local prev_line_str = vim.api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1] + if prev_line_str then + -- set onto last column of previous line, if possible. + pos[1] = pos[1] - 1 + -- # counts bytes, but win_set_cursor expects bytes, so all's good. + pos[2] = #prev_line_str + end else pos[2] = pos[2] - 1 end From bab59623b959899d2f4f9ca1108f5dda7b717270 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:57:29 +0100 Subject: [PATCH 50/69] document imperfect behaviour asserted by test. --- tests/integration/session_spec.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index 1b71a81b5..7a8b4cd92 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2246,6 +2246,9 @@ describe("session", function() {2:-- INSERT --recording @a} |]], }) feed("ccGqo@a") + -- this is not entirely correct!! + -- The autocommand that updated the docstring ("cc") of the original + -- snippet is disabled in the replayed snippet because. screen:expect({ grid = [[ /** | From 90c2fc63646d6366e925ad5240357855e14ef542 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:57:51 +0100 Subject: [PATCH 51/69] export optional_arg as opt for tests. --- tests/helpers.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers.lua b/tests/helpers.lua index 85ecedfe1..7caf0ff36 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -196,6 +196,7 @@ function M.session_setup_luasnip(opts) sp = require("luasnip.nodes.snippetProxy") pf = require("luasnip.extras.postfix").postfix k = require("luasnip.nodes.key_indexer").new_key + opt = require("luasnip.nodes.optional_arg").new_opt ]]) end From 79b985e10d340156bba3c4b6b80ee9ba2556338b Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 22:25:31 +0100 Subject: [PATCH 52/69] set jump_active=false ASAP. --- lua/luasnip/init.lua | 8 +++++--- lua/luasnip/util/feedkeys.lua | 7 +++++++ tests/integration/session_spec.lua | 5 +---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 99ddf47f5..260cb28a3 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -20,12 +20,14 @@ local log = require("luasnip.util.log").new("main") local function no_region_check_wrap(fn, ...) session.jump_active = true - -- will run on next tick, after autocommands (especially CursorMoved) for this are done. - vim.schedule(function() + local fn_res = fn(...) + -- once all movements and text-modifications (and autocommands triggered by + -- these) are done, we can set jump_active false, and allow the various + -- autocommands to change luasnip-state again. + feedkeys.enqueue_action(function() session.jump_active = false end) - local fn_res = fn(...) return fn_res end diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index b6d125eda..022c4d45e 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -58,6 +58,13 @@ local function enqueue_action(fn, keys_id) end end +function M.enqueue_action(fn) + enqueue_action(function(id) + fn() + M.confirm(id) + end, next_id()) +end + function M.feedkeys_insert(keys) enqueue_action(function(id) _feedkeys_insert(id, keys) diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index 7a8b4cd92..acb84bcec 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2246,9 +2246,6 @@ describe("session", function() {2:-- INSERT --recording @a} |]], }) feed("ccGqo@a") - -- this is not entirely correct!! - -- The autocommand that updated the docstring ("cc") of the original - -- snippet is disabled in the replayed snippet because. screen:expect({ grid = [[ /** | @@ -2267,7 +2264,7 @@ describe("session", function() * | * @return | * | - * @throws | + * @throws cc | */ | private aa bb() {4:●} | throws cc { | From 583befe2bbbe9176f7b9f3d22b5aa4f39158673c Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 19:37:17 +0100 Subject: [PATCH 53/69] choiceNode: explicitly set parent and pos for choices. Previously, we used a metatable to refer to the choiceNode for some keys that are required, which are only .parent and .pos. --- lua/luasnip/nodes/choiceNode.lua | 15 ++++++++------- lua/luasnip/nodes/dynamicNode.lua | 2 +- lua/luasnip/nodes/insertNode.lua | 2 +- lua/luasnip/nodes/node.lua | 12 ++++++------ lua/luasnip/nodes/restoreNode.lua | 2 +- lua/luasnip/nodes/snippet.lua | 2 +- lua/luasnip/nodes/util.lua | 4 ++-- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 68377f295..224abf4a7 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -17,12 +17,6 @@ function ChoiceNode:init_nodes() -- forward values for unknown keys from choiceNode. choice.choice = self - local node_mt = getmetatable(choice) - setmetatable(choice, { - __index = function(node, key) - return node_mt[key] or node.choice[key] - end, - }) choice.next_choice = self.choices[i + 1] choice.prev_choice = self.choices[i - 1] @@ -65,6 +59,13 @@ end extend_decorator.register(C, { arg_indx = 3 }) function ChoiceNode:subsnip_init() + for _, choice in ipairs(self.choices) do + choice.parent = self.parent + -- only insertNode needs this. + if choice.type == 2 or choice.type == 1 or choice.type == 3 then + choice.pos = self.pos + end + end node_util.subsnip_init_children(self.parent, self.choices) end @@ -168,7 +169,7 @@ end function ChoiceNode:get_docstring() return util.string_wrap( self.choices[1]:get_docstring(), - rawget(self, "pos") + self.pos ) end diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 9834bf4c2..1ace0f2e1 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -284,7 +284,7 @@ function DynamicNode:update_static() -- act as if snip is directly inside parent. tmp.parent = self.parent tmp.indx = self.indx - tmp.pos = rawget(self, "pos") + tmp.pos = self.pos tmp.next = self tmp.prev = self diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 00252a293..8825ff2a9 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -268,7 +268,7 @@ end function InsertNode:get_docstring() -- copy as to not in-place-modify static text. - return util.string_wrap(self:get_static_text(), rawget(self, "pos")) + return util.string_wrap(self:get_static_text(), self.pos) end function InsertNode:is_interactive() diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index f044c63f7..fc077514f 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -357,8 +357,8 @@ function Node:set_argnodes(dict) dict:set(self.absolute_insert_position, self) self.absolute_insert_position[#self.absolute_insert_position] = nil end - if rawget(self, "key") then - dict:set({ "key", rawget(self, "key"), "node" }, self) + if self.key then + dict:set({ "key", self.key, "node" }, self) end end @@ -598,20 +598,20 @@ function Node:linkable() -- linkable if insert or exitNode. return vim.tbl_contains( { types.insertNode, types.exitNode }, - rawget(self, "type") + self.type ) end function Node:interactive() -- interactive if immediately inside choiceNode. return vim.tbl_contains( { types.insertNode, types.exitNode }, - rawget(self, "type") - ) or rawget(self, "choice") ~= nil + self.type + ) or self.choice ~= nil end function Node:leaf() return vim.tbl_contains( { types.textNode, types.functionNode, types.insertNode, types.exitNode }, - rawget(self, "type") + self.type ) end diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index 945445e27..530b82a40 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -174,7 +174,7 @@ local function snip_init(self, snip) snip.snippet = self.parent.snippet -- pos should be nil if the restoreNode is inside a choiceNode. - snip.pos = rawget(self, "pos") + snip.pos = self.pos snip:resolve_child_ext_opts() snip:resolve_node_ext_opts() diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 34b80676e..1137b3db1 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1002,7 +1002,7 @@ function Snippet:get_docstring() -- function/dynamicNodes. -- if not outer snippet, wrap it in ${}. self.docstring = self.type == types.snippet and docstring - or util.string_wrap(docstring, rawget(self, "pos")) + or util.string_wrap(docstring, self.pos) return self.docstring end diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index dd30020bc..9e4d56aa4 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -187,7 +187,7 @@ local function linkable_node(node) -- node.type has to be one of insertNode, exitNode. return vim.tbl_contains( { types.insertNode, types.exitNode }, - rawget(node, "type") + node.type ) end @@ -200,7 +200,7 @@ end local function non_linkable_node(node) return vim.tbl_contains( { types.textNode, types.functionNode }, - rawget(node, "type") + node.type ) end -- return whether a node is certainly (not) interactive. From feeac25ccf7e849f1ebfdf9427c6df20aa745ad9 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 19:55:05 +0100 Subject: [PATCH 54/69] fix(dynamicNode): don't access .snip in update_static. --- lua/luasnip/nodes/dynamicNode.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 1ace0f2e1..598ec3b5d 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -259,7 +259,7 @@ function DynamicNode:update_static() self.fn, effective_args, self.parent, - self.snip.old_state, + self.static_snip.old_state, unpack(self.user_args) ) else From 733ccb266ed58d9fdf85a8dc8d5bcb81125b9964 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 20:46:56 +0100 Subject: [PATCH 55/69] dynamicNode: optionally use .snip to generate docstring. This may improve the accuracy of docstrings generated on an expanded snippet. --- lua/luasnip/nodes/dynamicNode.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 598ec3b5d..d0ccc72a4 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -66,6 +66,8 @@ function DynamicNode:get_docstring() if not self.docstring then if self.static_snip then self.docstring = self.static_snip:get_docstring() + elseif self.snip then + self.docstring = self.snip:get_docstring() else self.docstring = { "" } end From 211254b080310e652514b4b5eef694db623c6ceb Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 21:04:10 +0100 Subject: [PATCH 56/69] enqueue cursor-movement due to update in typeahead. Otherwise the jump_into from change_choice may complete after the cursor-movement due to the subsequent update. --- lua/luasnip/init.lua | 4 +-- lua/luasnip/util/feedkeys.lua | 11 +++++++ tests/integration/session_spec.lua | 49 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 260cb28a3..08f1ceb9c 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -200,9 +200,7 @@ local function restore_cursor_pos_relative(node, data) local selection_to = util.pos_add(node.mark:get_endpoint(1), data.selection_other_end_end_relative) feedkeys.select_range(selection_from, selection_to) else - util.set_cursor_0ind( - util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) - ) + feedkeys.move_to(util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative)) end end diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 022c4d45e..71aa8305f 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -151,6 +151,17 @@ function M.insert_at(pos) end, id) end +-- move, without changing mode. +function M.move_to(pos) + local id = next_id() + enqueued_cursor_state = {pos = pos, id = id} + + enqueue_action(function() + util.set_cursor_0ind(pos) + M.confirm(id) + end, id) +end + function M.confirm(id) executing_id = nil diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index acb84bcec..f99fc684b 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2280,4 +2280,53 @@ describe("session", function() |]], }) end) + + it("position is restored correctly after change_choice.", function() + feed("ifn") + expand() + jump(1) + jump(1) + jump(1) + jump(1) + change(1) + feed("asdf") + change(1) + change(1) + change(1) + -- currently wrong! +screen:expect({ + grid = [[ + /** | + * A short Description | + */ | + public void myFunc()^ { {4:●} | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} | + ]] +}) + end) end) From ae8d95c236989f87ce730d3f21bdf7fd30a33696 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 21:10:18 +0100 Subject: [PATCH 57/69] get_args: do (static_)visible-check in get_args, not get_static_text. Much more appropriate, also get_current_choices will work even if some insertNode is not visible. --- lua/luasnip/nodes/insertNode.lua | 6 ------ lua/luasnip/nodes/node.lua | 34 +++++++------------------------ tests/integration/choice_spec.lua | 13 ++++++++++++ 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 8825ff2a9..24b71ffb2 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -368,9 +368,6 @@ function InsertNode:get_snippetstring() return snippetstring end function InsertNode:get_static_snippetstring() - if not self.visible and not self.static_visible then - return nil - end return self.static_text end @@ -412,9 +409,6 @@ function InsertNode:put_initial(pos) end function InsertNode:get_static_text() - if not self.visible and not self.static_visible then - return nil - end return vim.split(self.static_text:str(), "\n") end diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index fc077514f..1b12c6cc2 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -32,28 +32,6 @@ function Node:new(o, opts) end function Node:get_static_text() - -- return nil if not visible. - -- This will prevent updates if not all nodes are visible during - -- docstring/static_text-generation. (One example that would otherwise fail - -- is the following snippet: - -- - -- s("trig", { - -- i(1, "cccc"), - -- t" ", - -- c(2, { - -- t"aaaa", - -- i(nil, "bbbb") - -- }), - -- f(function(args) return args[1][1]..args[2][1] end, {ai[2][2], 1} ) - -- }) - -- - -- ) - -- By also allowing visible, and not only static_visible, the docstrings - -- generated during `get_current_choices` (ie. without having the whole - -- snippet `static_init`ed) get better. - if not self.visible and not self.static_visible then - return nil - end return self.static_text end @@ -160,9 +138,6 @@ function Node:get_snippetstring() end function Node:get_static_snippetstring() - if not self.visible and not self.static_visible then - return nil - end -- if this is not overridden, get_static_text() is a multiline string. return snippet_string.new(self:get_static_text()) end @@ -275,8 +250,13 @@ local function get_args(node, get_text_func_name, static) dict_key[#dict_key] = nil end -- maybe the node is part of a dynamicNode and not yet generated. - if argnode then - if not static and argnode.visible then + -- also handle the argnode as not-present if + -- * we are doing a regular update and it is not visible, or + -- * we are doing a static update and it is not static_visible or + -- visible (this second condition is to allow the docstring-generation + -- to be improved by data provided after the expansion) + if argnode and ((static and (argnode.static_visible or argnode.visible)) or (not static and argnode.visible)) then + if not static then -- Don't store (aka call get_snippetstring) if this is a static -- update (there will be no associated buffer-region!) and -- don't store if the node is not visible. (Then there's diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index 34667c004..41b793a9b 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -309,4 +309,17 @@ describe("ChoiceNode", function() {2:-- INSERT --} |]], }) end) + + it("correctly gives current content of choices.", function() + assert.are.same({"${1:asdf}", "qwer"}, exec_lua[[ + ls.snip_expand(s("trig", { + c(1, { + i(1, "asdf"), + t"qwer" + }) + })) + ls.change_choice() + return ls.get_current_choices() + ]]) + end) end) From 3d8f8bdfe3e4e1e8a6cf465f8cc37dc11f48900a Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 23:28:31 +0100 Subject: [PATCH 58/69] test docstring-generation with self-dependent dynamicNode. --- tests/integration/dynamic_spec.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/integration/dynamic_spec.lua b/tests/integration/dynamic_spec.lua index 45d4125a6..afd2d3282 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -401,8 +401,9 @@ screen:expect({ end) it("selected text is selected again after updating (when possible).", function() - exec_lua[[ - ls.snip_expand(s("trig", { + + assert.are.same({"${1:${1:esdf}}$0"}, exec_lua[[ + snip = s("trig", { d(1, function(args) if not args[1] then return sn(nil, {i(1, "asdf", {key = "ins"})}) @@ -410,7 +411,11 @@ screen:expect({ return sn(nil, {i(1, args[1]:gsub("a", "e"), {key = "ins"})}) end end, {opt(k("ins"))}, {snippetstring_args = true}) - })) + }) + return snip:get_docstring() + ]]) + exec_lua[[ + ls.snip_expand(snip) ]] feed("a") exec_lua("ls.lsp_expand('${1:asdf}')") From 14280e1ea377a0bb3d48db86b4b23beb25bcbccb Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 7 Nov 2024 12:52:28 +0100 Subject: [PATCH 59/69] properly restore cursor-position in set_choice. * handle column-shifted begin-position (only shift cursor-column if it stays in the same line) * correctly enqueue cursor-movement via feedkeys. --- lua/luasnip/nodes/choiceNode.lua | 14 ++- lua/luasnip/util/util.lua | 9 ++ tests/integration/choice_spec.lua | 181 ++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 6 deletions(-) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 224abf4a7..881249c48 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -8,6 +8,8 @@ local mark = require("luasnip.util.mark").mark local session = require("luasnip.session") local sNode = require("luasnip.nodes.snippet").SN local extend_decorator = require("luasnip.util.extend_decorator") +local feedkeys = require("luasnip.util.feedkeys") +local log = require("luasnip.util.log").new("choice") function ChoiceNode:init_nodes() for i, choice in ipairs(self.choices) do @@ -233,8 +235,8 @@ function ChoiceNode:set_choice(choice, current_node) local insert_pre_cc = vim.fn.mode() == "i" -- is byte-indexed! Doesn't matter here, but important to be aware of. - local cursor_pos_pre_relative = - util.pos_sub(util.get_cursor_0ind(), current_node.mark:pos_begin_raw()) + local cursor_node_offset = + util.pos_offset(current_node.mark:pos_begin(), util.get_cursor_0ind()) self.active_choice:store() @@ -289,10 +291,10 @@ function ChoiceNode:set_choice(choice, current_node) node_util.refocus(self, target_node) if insert_pre_cc then - util.set_cursor_0ind( - util.pos_add( - target_node.mark:pos_begin_raw(), - cursor_pos_pre_relative + feedkeys.move_to( + util.pos_from_offset( + target_node.mark:pos_begin(), + cursor_node_offset ) ) else diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index b4a58ebcc..90010efa9 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -397,6 +397,14 @@ local function pos_offset(base_pos, pos) return {row_offset, row_offset == 0 and pos[2] - base_pos[2] or pos[2]} end +-- compute offset of `pos` into multiline string starting at `base_pos`. +-- This is different from pos_sub because here the column-offset starts at zero +-- when `pos` is on a line different from `base_pos`. +-- Assumption: `pos` occurs after `base_pos`. +local function pos_from_offset(base_pos, offset) + return {base_pos[1]+offset[1], offset[1] == 0 and base_pos[2] + offset[2] or offset[2]} +end + local function shallow_copy(t) if type(t) == "table" then local res = {} @@ -449,5 +457,6 @@ return { ternary = ternary, pos_cmp = pos_cmp, pos_offset = pos_offset, + pos_from_offset = pos_from_offset, shallow_copy = shallow_copy } diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index 41b793a9b..ec25c9109 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -322,4 +322,185 @@ describe("ChoiceNode", function() return ls.get_current_choices() ]]) end) + + it("correctly restores the generated node of a dynamicNode.", function() + assert.are.same({ "${1:${${1:aaa}${2:${1:aaa}}}}$0" }, exec_lua[[ + snip = s("trig", { + c(1, { + r(nil, "restore_key", { + i(1, "aaa"), d(2, function(args) return sn(nil, {i(1, args[1])}) end, {1}, {snippetstring_args = true}) + }), + { + t"a", + r(1, "restore_key"), + t"a" + } + }) + }) + return snip:get_docstring() + ]]) + exec_lua("ls.snip_expand(snip)") + feed("qwer") + exec_lua("ls.jump(1)") +screen:expect({ + grid = [[ + qwer^q{3:wer} | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua("ls.change_choice(1)") +screen:expect({ + grid = [[ + a^q{3:wer}qwera | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + end) + + it("cursor is correctly restored after change", function() + screen:detach() + + ls_helpers.clear() + ls_helpers.session_setup_luasnip() + + screen = Screen.new(50, 7) + screen:attach() + screen:set_default_attr_ids({ + [0] = { bold = true, foreground = Screen.colors.Blue }, + [1] = { bold = true, foreground = Screen.colors.Brown }, + [2] = { bold = true }, + [3] = { background = Screen.colors.LightGray }, + }) + + exec_lua[=[ + ls.snip_expand(s("trig", { + c(1, { + fmt([[ + local {} = function() + {} + end + ]], {r(1, "name", i(1, "fname")), sn(2, {t{"aaaa", "bbbb"},r(1, "body", i(1, "fbody"))}) }), + fmt([[ + local function {}() + {} + end + ]], {r(1, "name", i(1, "fname")), r(2, "body", i(1, "fbody"))}) + }, {restore_cursor = true}) + })) + ]=] + exec_lua("vim.wait(10, function() end)") + + exec_lua"ls.jump(1)" + feed("asdfasdfqweraaaa") +screen:expect({ + grid = [[ + local fname = function() | + aaaa | + bbbbasdf | + asdf | + qwer | + aa^aa | + {2:-- INSERT --} | + ]] +}) + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + local function fname() | + asdf | + asdf | + qwer | + aa^aa | + end | + {2:-- INSERT --} | + ]] +}) + exec_lua"ls.jump(-1)" + exec_lua"ls.jump(1)" +screen:expect({ + grid = [[ + local function fname() | + ^a{3:sdf} | + {3:asdf} | + {3:qwer} | + {3: aaaa} | + end | + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + aaaa | + bbbb^a{3:sdf} | + {3:asdf} | + {3:qwer} | + {3: aaaa} | + end | + {2:-- SELECT --} | + ]] +}) + feed("i") + exec_lua"ls.change_choice(1)" + exec_lua[=[ + ls.snip_expand(s("for", { + t"for ", c(1, { + sn(nil, {i(1, "k"), t", ", i(2, "v"), t" in ", c(3, {{t"pairs(",i(1),t")"}, {t"ipairs(",i(1),t")"}, i(nil)}, {restore_cursor = true}) }), + sn(nil, {i(1, "val"), t" in ", i(2) }), + sn(nil, {i(1, "i"), t" = ", i(2), t", ", i(3) }), + fmt([[{} in vim.gsplit({})]], {i(1, "str"), i(2)}) + }, {restore_cursor = true}), t{" do", "\t"}, isn(2, {dl(1, l.LS_SELECT_DEDENT)}, "$PARENT_INDENT\t"), t{"", "end"} + })) + ]=] +screen:expect({ + grid = [[ + local function fname() | + for ^k, v in pairs() do | + | + endi | + end | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + local function fname() | + for ^v{3:al} in do | + | + endi | + end | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1)" + exec_lua"ls.jump(1)" +screen:expect({ + grid = [[ + local function fname() | + for val in do | + ^ | + endi | + end | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + local fname = function() | + aaaa | + bbbbfor val in do | + ^ | + endi | + end | + {2:-- INSERT --} | + ]] +}) + end) end) From 1464b3fff4ca6b84ea066b0b888520e71c1b140a Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 7 Nov 2024 19:34:14 +0100 Subject: [PATCH 60/69] change_choice: use cursor-restore system from update_dependents. Code is essentially the same thing. Also allow passing the current cursor to ls.set/change_choice, which is useful for extras.select_choice, where the cursor-state is "destroyed" due to vim.input, and should be saved before that is even opened. --- lua/luasnip/extras/select_choice.lua | 20 +++++--- lua/luasnip/init.lua | 73 +++++++--------------------- lua/luasnip/nodes/choiceNode.lua | 23 ++------- lua/luasnip/nodes/util.lua | 57 +++++++++++++++++++++- lua/luasnip/util/feedkeys.lua | 34 +++++++------ tests/integration/choice_spec.lua | 30 ++++++++++++ 6 files changed, 142 insertions(+), 95 deletions(-) diff --git a/lua/luasnip/extras/select_choice.lua b/lua/luasnip/extras/select_choice.lua index adca3a2ad..b7b55e1a2 100644 --- a/lua/luasnip/extras/select_choice.lua +++ b/lua/luasnip/extras/select_choice.lua @@ -1,13 +1,16 @@ local session = require("luasnip.session") local ls = require("luasnip") +local node_util = require("luasnip.nodes.util") -local function set_choice_callback(_, indx) - if not indx then - return +local function set_choice_callback(data) + return function(_, indx) + if not indx then + return + end + -- feed+immediately execute i to enter INSERT after vim.ui.input closes. + -- vim.api.nvim_feedkeys("i", "x", false) + ls.set_choice(indx, {cursor_restore_data = data}) end - -- feed+immediately execute i to enter INSERT after vim.ui.input closes. - vim.api.nvim_feedkeys("i", "x", false) - ls.set_choice(indx) end local function select_choice() @@ -15,10 +18,13 @@ local function select_choice() session.active_choice_nodes[vim.api.nvim_get_current_buf()], "No active choiceNode" ) + local active = session.current_nodes[vim.api.nvim_get_current_buf()] + + local restore_data = node_util.store_cursor_node_relative(active) vim.ui.select( ls.get_current_choices(), { kind = "luasnip" }, - set_choice_callback + set_choice_callback(restore_data) ) end diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 08f1ceb9c..6d5c090f2 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -149,43 +149,6 @@ local function unlink_current() unlink_set_adjacent_as_current_no_log(current.parent.snippet) end -local store_id = 0 -local function store_cursor_node_relative(node) - local data = {} - - local snippet_current_node = node - - -- store for each snippet! - -- the innermost snippet may be destroyed, and we would have to restore the - -- cursor in a snippet above that. - while snippet_current_node do - local snip = snippet_current_node:get_snippet() - - local snip_data = {} - - snip_data.key = node.key - node.store_id = store_id - snip_data.store_id = store_id - snip_data.node = snippet_current_node - - store_id = store_id + 1 - - local cursor_state = feedkeys.last_state() - snip_data.cursor_end_relative = - util.pos_sub(cursor_state.pos, node.mark:get_endpoint(1)) - - if cursor_state.pos_end then - snip_data.selection_other_end_end_relative = util.pos_sub(cursor_state.pos_end, node.mark:get_endpoint(1)) - end - - data[snip] = snip_data - - snippet_current_node = snip:get_snippet().parent_node - end - - return data -end - local function get_corresponding_node(parent, data) return parent:find_node(function(test_node) return (test_node.store_id == data.store_id) @@ -193,19 +156,8 @@ local function get_corresponding_node(parent, data) end, {find_in_child_snippets = true}) end -local function restore_cursor_pos_relative(node, data) - if data.selection_other_end_end_relative then - -- is a selection => restore it. - local selection_from = util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative) - local selection_to = util.pos_add(node.mark:get_endpoint(1), data.selection_other_end_end_relative) - feedkeys.select_range(selection_from, selection_to) - else - feedkeys.move_to(util.pos_add(node.mark:get_endpoint(1), data.cursor_end_relative)) - end -end - local function node_update_dependents_preserve_position(node, current, opts) - local restore_data = store_cursor_node_relative(current) + local restore_data = node_util.store_cursor_node_relative(current) -- update all nodes that depend on this one. local ok, res = @@ -230,7 +182,7 @@ local function node_update_dependents_preserve_position(node, current, opts) if not opts.no_move and opts.restore_position then -- node is visible: restore position. local active_snippet = current:get_snippet() - restore_cursor_pos_relative(current, restore_data[active_snippet]) + node_util.restore_cursor_pos_relative(current, restore_data[active_snippet]) end return { jump_done = false, new_current = current } @@ -275,7 +227,7 @@ local function node_update_dependents_preserve_position(node, current, opts) if not opts.no_move and opts.restore_position then -- node is visible: restore position - restore_cursor_pos_relative(new_current, snip_restore_data) + node_util.restore_cursor_pos_relative(new_current, snip_restore_data) end return { jump_done = false, new_current = new_current } @@ -621,23 +573,33 @@ local function safe_choice_action(snip, ...) return session.current_nodes[vim.api.nvim_get_current_buf()] end end -local function change_choice(val) +local function change_choice(val, opts) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] + assert(active_choice, "No active choiceNode") + + -- if the active choice exists current_node still does. + local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] + local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node) + local new_active = no_region_check_wrap( safe_choice_action, active_choice.parent.snippet, active_choice.change_choice, active_choice, val, - session.current_nodes[vim.api.nvim_get_current_buf()] + session.current_nodes[vim.api.nvim_get_current_buf()], + restore_data ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active active_update_dependents() end -local function set_choice(choice_indx) +local function set_choice(choice_indx, opts) + local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] + local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node) + local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] assert(active_choice, "No active choiceNode") @@ -649,7 +611,8 @@ local function set_choice(choice_indx) active_choice.set_choice, active_choice, choice, - session.current_nodes[vim.api.nvim_get_current_buf()] + current_node, + restore_data ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active active_update_dependents() diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 881249c48..777c56070 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -227,17 +227,12 @@ end -- used to uniquely identify this change-choice-action. local change_choice_id = 0 -function ChoiceNode:set_choice(choice, current_node) +function ChoiceNode:set_choice(choice, current_node, cursor_restore_data) change_choice_id = change_choice_id + 1 -- to uniquely identify this node later (storing the pointer isn't enough -- because this is supposed to work with restoreNodes, which are copied). current_node.change_choice_id = change_choice_id - local insert_pre_cc = vim.fn.mode() == "i" - -- is byte-indexed! Doesn't matter here, but important to be aware of. - local cursor_node_offset = - util.pos_offset(current_node.mark:pos_begin(), util.get_cursor_0ind()) - self.active_choice:store() -- tear down current choice. @@ -289,17 +284,8 @@ function ChoiceNode:set_choice(choice, current_node) -- and this choiceNode, then set the cursor. node_util.refocus(self, target_node) + node_util.restore_cursor_pos_relative(target_node, cursor_restore_data[target_node.parent.snippet]) - if insert_pre_cc then - feedkeys.move_to( - util.pos_from_offset( - target_node.mark:pos_begin(), - cursor_node_offset - ) - ) - else - node_util.select_node(target_node) - end return target_node end end @@ -307,12 +293,13 @@ function ChoiceNode:set_choice(choice, current_node) return self.active_choice:jump_into(1) end -function ChoiceNode:change_choice(dir, current_node) +function ChoiceNode:change_choice(dir, current_node, cursor_restore_data) -- stylua: ignore return self:set_choice( dir == 1 and self.active_choice.next_choice or self.active_choice.prev_choice, - current_node ) + current_node, + cursor_restore_data) end function ChoiceNode:copy() diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 9e4d56aa4..8a1ef1d31 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -862,6 +862,59 @@ local function str_args(args) end, args) end +local store_id = 0 +local function store_cursor_node_relative(node) + local data = {} + + local snippet_current_node = node + + local cursor_state = feedkeys.last_state() + + -- store for each snippet! + -- the innermost snippet may be destroyed, and we would have to restore the + -- cursor in a snippet above that. + while snippet_current_node do + local snip = snippet_current_node:get_snippet() + + local snip_data = {} + + snip_data.key = node.key + node.store_id = store_id + snip_data.store_id = store_id + snip_data.node = snippet_current_node + + store_id = store_id + 1 + + snip_data.cursor_start_relative = + util.pos_offset(node.mark:get_endpoint(-1), cursor_state.pos) + + snip_data.mode = cursor_state.mode + + if cursor_state.pos_v then + snip_data.selection_end_start_relative = util.pos_offset(node.mark:get_endpoint(-1), cursor_state.pos_v) + end + + data[snip] = snip_data + + snippet_current_node = snip:get_snippet().parent_node + end + + return data +end + +local function restore_cursor_pos_relative(node, data) + if data.mode == "i" then + feedkeys.insert_at(util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative)) + elseif data.mode == "s" then + -- is a selection => restore it. + local selection_from = util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative) + local selection_to = util.pos_from_offset(node.mark:get_endpoint(-1), data.selection_end_start_relative) + feedkeys.select_range(selection_from, selection_to) + else + feedkeys.move_to_normal(util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative)) + end +end + return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -887,5 +940,7 @@ return { find_node_dependents = find_node_dependents, collect_dependents = collect_dependents, node_subtree_do = node_subtree_do, - str_args = str_args + str_args = str_args, + store_cursor_node_relative = store_cursor_node_relative, + restore_cursor_pos_relative = restore_cursor_pos_relative } diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 71aa8305f..6ed6c1699 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -99,7 +99,7 @@ end function M.select_range(b, e) local id = next_id() - enqueued_cursor_state = {pos = vim.deepcopy(b), pos_end = vim.deepcopy(e), id = id} + enqueued_cursor_state = {pos = vim.deepcopy(b), pos_v = vim.deepcopy(e), mode = "s", id = id} enqueue_action(function() -- stylua: ignore _feedkeys_insert(id, @@ -132,7 +132,7 @@ end -- move the cursor to a position and enter insert-mode (or stay in it). function M.insert_at(pos) local id = next_id() - enqueued_cursor_state = {pos = pos, id = id} + enqueued_cursor_state = {pos = pos, mode = "i", id = id} enqueue_action(function() -- if current and target mode is INSERT, there's no reason to leave it. @@ -152,13 +152,18 @@ function M.insert_at(pos) end -- move, without changing mode. -function M.move_to(pos) +function M.move_to_normal(pos) local id = next_id() - enqueued_cursor_state = {pos = pos, id = id} + -- preserve mode. + enqueued_cursor_state = {pos = pos, mode = "n", id = id} enqueue_action(function() - util.set_cursor_0ind(pos) - M.confirm(id) + if vim.fn.mode():sub(1,1) == "n" then + util.set_cursor_0ind(pos) + M.confirm(id) + else + _feedkeys_insert(id, "" .. cursor_set_keys(pos)) + end end, id) end @@ -181,6 +186,7 @@ end function M.last_state() if enqueued_cursor_state then local state = vim.deepcopy(enqueued_cursor_state) + -- remove internal data. state.id = nil return state end @@ -190,14 +196,14 @@ function M.last_state() local getposdot = vim.fn.getpos(".") state.pos = {getposdot[2]-1, getposdot[3]-1} - -- only re-enter select for now. - if vim.fn.mode() == "s" then - local getposv = vim.fn.getpos("v") - -- store selection-range with end-position one column after the cursor - -- at the end (so -1 to make getpos-position 0-based, +1 to move it one - -- beyond the last character of the range) - state.pos_end = {getposv[2]-1, getposv[3]+1} - end + local getposv = vim.fn.getpos("v") + -- store selection-range with end-position one column after the cursor + -- at the end (so -1 to make getpos-position 0-based, +1 to move it one + -- beyond the last character of the range) + state.pos_v = {getposv[2]-1, getposv[3]} + + -- only store first component. + state.mode = vim.fn.mode():sub(1,1) return state end diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index ec25c9109..3b4d28d1d 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -501,6 +501,36 @@ screen:expect({ end | {2:-- INSERT --} | ]] +}) + end) + + it("select_choice works.", function() + exec_lua[=[ + ls.snip_expand(s("for", { + t"for ", c(1, { + sn(nil, {i(1, "k"), t", ", i(2, "v"), t" in ", c(3, {{t"pairs(",i(1),t")"}, {t"ipairs(",i(1),t")"}, i(nil)}, {restore_cursor = true}) }), + sn(nil, {i(1, "val"), t" in ", i(2) }), + sn(nil, {i(1, "i"), t" = ", i(2), t", ", i(3) }), + fmt([[{} in vim.gsplit({})]], {i(1, "str"), i(2)}) + }, {restore_cursor = true}), t{" do", "\t"}, isn(2, {dl(1, l.LS_SELECT_DEDENT)}, "$PARENT_INDENT\t"), t{"", "end"} + })) + ]=] + feed("lua require('luasnip.extras.select_choice')()2") +screen:expect({ + grid = [[ + for ^v{3:al} in do | + | + {2:-- SELECT --} | + ]] +}) + -- re-selecting correctly highlights text again (test by editing so the test does not pass immediately, without any changes!) + feed("lua require('luasnip.extras.select_choice')()2val") +screen:expect({ + grid = [[ + for val^ in do | + | + {2:-- INSERT --} | + ]] }) end) end) From f78bf7424067a884a2a58673de16294761d9af33 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 13 Nov 2024 11:58:28 +0100 Subject: [PATCH 61/69] snippet_string: add metadata and marks. metadata can store eg. when a snippetString was created, marks are a bit like extmarks, they can mark a position in a snippetString and are shifted by text-insertions. --- lua/luasnip/nodes/util/snippet_string.lua | 95 ++++++++++++++++++++++- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index bba59c7cf..323c7f5eb 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -5,6 +5,8 @@ local util = require("luasnip.util.util") local SnippetString = {} local SnippetString_mt = { __index = SnippetString, + + -- __concat and __tostring will be set later on. } local M = {} @@ -12,8 +14,8 @@ local M = {} ---Create new SnippetString. ---@param initial_str string[]?, optional initial multiline string. ---@return SnippetString -function M.new(initial_str) - local o = {initial_str and table.concat(initial_str, "\n")} +function M.new(initial_str, metadata) + local o = {initial_str and table.concat(initial_str, "\n"), marks = {}, metadata = metadata} return setmetatable(o, SnippetString_mt) end @@ -120,7 +122,7 @@ function SnippetString:put(pos) end function SnippetString:copy() - -- on 0.7 vim.deepcopy does not behave correctly => have to manually copy. + -- on 0.7 vim.deepcopy does not behave correctly on snippets => have to manually copy. return setmetatable(vim.tbl_map(function(snipstr_or_str) if snipstr_or_str.snip then local snip = snipstr_or_str.snip @@ -158,7 +160,8 @@ function SnippetString:copy() return {snip = snipcop} else - return snipstr_or_str + -- handles raw strings and marks and metadata + return vim.deepcopy(snipstr_or_str) end end, self), SnippetString_mt) end @@ -169,6 +172,9 @@ function SnippetString:flatcopy() for i, v in ipairs(self) do res[i] = util.shallow_copy(v) end + -- we simply copy marks including their id's. + res.marks = vim.deepcopy(self.marks) + res.metadata = vim.deepcopy(self.metadata) return setmetatable(res, SnippetString_mt) end @@ -188,6 +194,28 @@ function SnippetString.concat(a, b) b = to_snippetstring(b):flatcopy() vim.list_extend(a, b) + -- now, this means we may have duplicated mark-ids. + -- I think this is okay because we will simply always return the first + -- occurence of some id. + -- + -- An alternative would be to modify the mark-ids to be non-overlapping, but + -- then we may not be able to retrieve all marks. + for _, mark in ipairs(b.marks) do + -- bit wasteful to compute a:str here. + -- Think about caching the total length of the snippetString. + mark.pos = mark.pos + #a:str() + end + + vim.list_extend(a.marks, b.marks) + + -- overwrite metadata from a. + -- I don't think this will be a problem for the usecase of storing the + -- luasnip_changedtick, since all snippetStrings present in some + -- dynamicNode will have the same changedtick. + for k, v in pairs(b.metadata) do + a.metadata[k] = v + end + return a end SnippetString_mt.__concat = SnippetString.concat @@ -328,6 +356,28 @@ local function _replace(self, replacements, snipstr_map) -- start-position of string has to be updated. snipstr_map[self][v_i_from] = v_from_from end + + -- update marks. + -- take note that repl_from and repl_to are given wrt. the outermost + -- snippet_string, and mark.pos is relative to self. + -- So, these have to be converted to and from. + local self_offset = snipstr_map[self][1]-1 + for _, mark in ipairs(self.marks) do + if repl.to < mark.pos + self_offset then + -- mark shifted to the right. + mark.pos = mark.pos - (repl.to - repl.from+1) + #repl.str + elseif repl.from < mark.pos + self_offset then + -- we already know that repl.to >= mark.pos. + -- This means that the marker is inside the deleted region, and + -- we have to somehow find a sensible new position. + -- For now, simply preserve the marks position if the new str + -- still covers the region, otherwise shift it to the beginning + -- or end of the newly inserted text, depending on rgrav. + mark.pos = (mark.rgrav and repl.to+1 or repl.from) - self_offset + end + -- in this case the replacement is completely behind the marks + -- position, don't have to change it. + end end end @@ -465,4 +515,41 @@ function SnippetString:sub(from, to) end +-- add a kind-of extmark to the text in this buffer. It moves with inserted +-- text, and has a gravity to control into which direction it shifts. +-- pos is 1-based and refers to one character in the string, rgrav = true can be +-- understood as the mark being incident with the characters right edge (replace +-- character at pos with multiple characters => mark will move to the right of +-- the newly inserted chars), and rgrav = false with the left edge (replace char +-- with multiple chars => mark stays at char). +-- If the edge is in the middle of multiple characters (for example rgrav=true, +-- and chars at pos and pos+1 are replaced), the mark is removed. +function SnippetString:add_mark(id, pos, rgrav) + -- I'd expect there to be at most 0-2 marks in any given static_text, which + -- are those set to track the cursor-position. + -- We can thus use a flat array in favor of more complicated data + -- structures. + -- Internally, treat all marks as sticking to the left edge of their + -- respective character, and simply +1 or -1 them to match gravity + -- (rgrav=true @ pos === rgrav=false @ pos+1). + -- gravity still has to be stored to correctly return the marks position + -- when it is retrieved. + table.insert(self.marks, { + id = id, + pos = pos + (rgrav and 1 or 0), + rgrav = rgrav}) +end + +function SnippetString:get_mark_pos(id) + for _, mark in ipairs(self.marks) do + if mark.id == id then + return mark.pos - (mark.rgrav and 1 or 0) + end + end +end + +function SnippetString:clear_marks() + self.marks = {} +end + return M From d553a79f36006b22321957c2ed042ce06683a93e Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 13 Nov 2024 12:06:34 +0100 Subject: [PATCH 62/69] store cursor-position in snippetString to more accurately restore it. Cache `static_text` (for insertNodes) and don't query it anew while luasnip is operating. This is valid under the assumption that all changes to the buffer are due to luasnip while an api-function (jump, expand, etc.) is running. This is enabled by session.luasnip_changedtick which is set as soon as an api-function is called, and prevents re-fetching snippetStrings from the buffer (which in turn allows us to set the cursor-position once, and then have it propagate through all updates that are triggered subsequently). This commit also replaces no_region_check_wrap with api_do, which is more general (handles both jump_active, which prevents recursive api-calls and luasnip_changedtick) --- lua/luasnip/extras/select_choice.lua | 9 +- lua/luasnip/init.lua | 216 +++++++++++++++------- lua/luasnip/nodes/choiceNode.lua | 2 +- lua/luasnip/nodes/insertNode.lua | 25 ++- lua/luasnip/nodes/node.lua | 20 +- lua/luasnip/nodes/util.lua | 99 ++++++++-- lua/luasnip/nodes/util/snippet_string.lua | 6 +- lua/luasnip/session/init.lua | 14 +- lua/luasnip/util/str.lua | 50 +++++ tests/integration/snippet_basics_spec.lua | 2 +- tests/unit/str_spec.lua | 69 +++++++ 11 files changed, 402 insertions(+), 110 deletions(-) diff --git a/lua/luasnip/extras/select_choice.lua b/lua/luasnip/extras/select_choice.lua index b7b55e1a2..80af8c345 100644 --- a/lua/luasnip/extras/select_choice.lua +++ b/lua/luasnip/extras/select_choice.lua @@ -2,14 +2,18 @@ local session = require("luasnip.session") local ls = require("luasnip") local node_util = require("luasnip.nodes.util") +-- in this procedure, make sure that api_leave is called before +-- set_choice_callback exits. local function set_choice_callback(data) return function(_, indx) if not indx then + ls._api_leave() return end -- feed+immediately execute i to enter INSERT after vim.ui.input closes. -- vim.api.nvim_feedkeys("i", "x", false) - ls.set_choice(indx, {cursor_restore_data = data}) + ls._set_choice(indx, {cursor_restore_data = data}) + ls._api_leave() end end @@ -20,7 +24,8 @@ local function select_choice() ) local active = session.current_nodes[vim.api.nvim_get_current_buf()] - local restore_data = node_util.store_cursor_node_relative(active) + ls._api_enter() + local restore_data = node_util.store_cursor_node_relative(active, {place_cursor_mark = false}) vim.ui.select( ls.get_current_choices(), { kind = "luasnip" }, diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 6d5c090f2..c01a19e67 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -2,6 +2,7 @@ local util = require("luasnip.util.util") local lazy_table = require("luasnip.util.lazy_table") local types = require("luasnip.util.types") local node_util = require("luasnip.nodes.util") +local tbl_util = require("luasnip.util.table") local feedkeys = require("luasnip.util.feedkeys") local session = require("luasnip.session") @@ -18,15 +19,28 @@ local luasnip_data_dir = vim.fn.stdpath("cache") .. "/luasnip" local log = require("luasnip.util.log").new("main") -local function no_region_check_wrap(fn, ...) +local luasnip_changedtick = 0 +local function api_enter() session.jump_active = true - local fn_res = fn(...) + assert(session.luasnip_changedtick == nil) + session.luasnip_changedtick = luasnip_changedtick + luasnip_changedtick = luasnip_changedtick + 1 +end +local function api_leave() -- once all movements and text-modifications (and autocommands triggered by -- these) are done, we can set jump_active false, and allow the various -- autocommands to change luasnip-state again. feedkeys.enqueue_action(function() session.jump_active = false end) + session.luasnip_changedtick = nil +end + +local function api_do(fn, ...) + api_enter() + + local fn_res = fn(...) + api_leave() return fn_res end @@ -149,19 +163,14 @@ local function unlink_current() unlink_set_adjacent_as_current_no_log(current.parent.snippet) end -local function get_corresponding_node(parent, data) - return parent:find_node(function(test_node) - return (test_node.store_id == data.store_id) - or (data.key ~= nil and test_node.key == data.key) - end, {find_in_child_snippets = true}) -end - local function node_update_dependents_preserve_position(node, current, opts) - local restore_data = node_util.store_cursor_node_relative(current) + -- set luasnip_changedtick so that static_text is preserved when possible. + local restore_data = node_util.store_cursor_node_relative(current, {place_cursor_mark = true}) -- update all nodes that depend on this one. local ok, res = pcall(node.update_dependents, node, { own = true, parents = true }) + if not ok then local snip = node:get_snippet() @@ -182,7 +191,7 @@ local function node_update_dependents_preserve_position(node, current, opts) if not opts.no_move and opts.restore_position then -- node is visible: restore position. local active_snippet = current:get_snippet() - node_util.restore_cursor_pos_relative(current, restore_data[active_snippet]) + node_util.restore_cursor_pos_relative(current, restore_data[active_snippet.node_store_id]) end return { jump_done = false, new_current = current } @@ -200,9 +209,9 @@ local function node_update_dependents_preserve_position(node, current, opts) active_snippet = parent_node:get_snippet() end - -- have found first visible snippet => look for dynamicNode. - local snip_restore_data = restore_data[active_snippet] - local node_parent = snip_restore_data.node.parent + -- have found first visible snippet => look for visible dynamicNode, + -- starting from which we can try to find a new active node. + local node_parent = restore_data[active_snippet.node_store_id].node.parent -- find visible dynamicNode that contained the (now-inactive) insertNode. -- since the node was no longer visible after an update, it must have @@ -220,14 +229,45 @@ local function node_update_dependents_preserve_position(node, current, opts) "Visible dynamicNode that was a parent of the current node is not active after the update!! If you get this message, please open an issue with LuaSnip!" ) - local new_current = get_corresponding_node(d, snip_restore_data) + local found_nodes = {} + d:subtree_do({ + pre = function(sd_node) + if sd_node.key then + -- any snippet we encounter here was generated before, and + -- if sd_node has the correct key, its snippet has a + -- node_store_id that corresponds to it. + local snip_node_store_id = sd_node.parent.snippet.node_store_id + -- make sure that the key we found belongs to this + -- snippets' active node. + -- Also use the first valid node, and not the second one. + -- Doesn't really matter (ambiguous keys -> undefined + -- behaviour), but we should just use the first one, as + -- that seems more like what would be expected. + if snip_node_store_id and restore_data[snip_node_store_id] and sd_node.key == restore_data[snip_node_store_id].key and not found_nodes[snip_node_store_id] then + found_nodes[snip_node_store_id] = sd_node + end + elseif sd_node.store_id and restore_data[sd_node.store_id] and not found_nodes[sd_node.store_id] then + found_nodes[sd_node.store_id] = sd_node + end + end, + post=util.nop, + do_child_snippets=true + }) + + local new_current + for _, store_id in ipairs(restore_data.store_ids) do + if found_nodes[store_id] then + new_current = found_nodes[store_id] + break + end + end if new_current then node_util.refocus(d, new_current) if not opts.no_move and opts.restore_position then -- node is visible: restore position - node_util.restore_cursor_pos_relative(new_current, snip_restore_data) + node_util.restore_cursor_pos_relative(new_current, restore_data[new_current.parent.snippet.node_store_id]) end return { jump_done = false, new_current = new_current } @@ -252,14 +292,20 @@ local function update_dependents(node) active, { no_move = false, restore_position = true } ) - upd_res.new_current:focus() - session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_current + if upd_res.new_current then + upd_res.new_current:focus() + session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_current + end end end -local function active_update_dependents() +local function _active_update_dependents() update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()]) end +local function active_update_dependents() + api_do(_active_update_dependents) +end + -- return next active node. local function safe_jump_current(dir, no_move, dry_run) local node = session.current_nodes[vim.api.nvim_get_current_buf()] @@ -289,10 +335,10 @@ local function safe_jump_current(dir, no_move, dry_run) end end -local function jump(dir) +local function _jump(dir) local current = session.current_nodes[vim.api.nvim_get_current_buf()] if current then - local next_node = no_region_check_wrap(safe_jump_current, dir) + local next_node = safe_jump_current(dir) if next_node == nil then session.current_nodes[vim.api.nvim_get_current_buf()] = nil return true @@ -309,6 +355,11 @@ local function jump(dir) return false end end + +local function jump(dir) + return api_do(_jump, dir) +end + local function jump_destination(dir) -- dry run of jump (+no_move ofc.), only retrieves destination-node. return safe_jump_current(dir, true, { active = {} }) @@ -365,11 +416,11 @@ local function locally_jumpable(dir) end local function _jump_into_default(snippet) - return no_region_check_wrap(snippet.jump_into, snippet, 1) + return snippet:jump_into(1) end -- opts.clear_region: table, keys `from` and `to`, both (0,0)-indexed. -local function snip_expand(snippet, opts) +local function _snip_expand(snippet, opts) local snip = snippet:copy() opts = opts or {} @@ -448,16 +499,20 @@ local function snip_expand(snippet, opts) -- schedule update of active node. -- Not really happy with this, but for some reason I don't have time to -- investigate, nvim_buf_get_text does not return the updated text :/ - active_update_dependents() + _active_update_dependents() return snip end +local function snip_expand(snippet, opts) + return api_do(_snip_expand, snippet, opts) +end + ---Find a snippet matching the current cursor-position. ---@param opts table: may contain: --- - `jump_into_func`: passed through to `snip_expand`. ---@return boolean: whether a snippet was expanded. -local function expand(opts) +local function _expand(opts) local expand_params local snip -- find snip via next_expand (set from previous expandable()) or manual matching. @@ -486,7 +541,7 @@ local function expand(opts) } -- override snip with expanded copy. - snip = snip_expand(snip, { + snip = _snip_expand(snip, { expand_params = expand_params, -- clear trigger-text. clear_region = clear_region, @@ -498,49 +553,59 @@ local function expand(opts) return false end +local function expand(opts) + return api_do(_expand, opts) +end + local function expand_auto() - local snip, expand_params = - match_snippet(util.get_current_line_to_cursor(), "autosnippets") - if snip then - local cursor = util.get_cursor_0ind() - local clear_region = expand_params.clear_region - or { - from = { - cursor[1], - cursor[2] - #expand_params.trigger, - }, - to = cursor, - } - snip = snip_expand(snip, { - expand_params = expand_params, - -- clear trigger-text. - clear_region = clear_region, - }) - end + api_do(function() + local snip, expand_params = + match_snippet(util.get_current_line_to_cursor(), "autosnippets") + if snip then + local cursor = util.get_cursor_0ind() + local clear_region = expand_params.clear_region + or { + from = { + cursor[1], + cursor[2] - #expand_params.trigger, + }, + to = cursor, + } + snip = _snip_expand(snip, { + expand_params = expand_params, + -- clear trigger-text. + clear_region = clear_region, + }) + end + end) end local function expand_repeat() - -- prevent clearing text with repeated expand. - session.last_expand_opts.clear_region = nil - session.last_expand_opts.pos = nil + api_do(function() + -- prevent clearing text with repeated expand. + session.last_expand_opts.clear_region = nil + session.last_expand_opts.pos = nil - snip_expand(session.last_expand_snip, session.last_expand_opts) + _snip_expand(session.last_expand_snip, session.last_expand_opts) + end) end -- return true and expand snippet if expandable, return false if not. local function expand_or_jump() - if expand() then - return true - end - if jump(1) then - return true - end - return false + return api_do(function() + if _expand() then + return true + end + if _jump(1) then + return true + end + return false + end) end local function lsp_expand(body, opts) -- expand snippet as-is. - snip_expand( + api_do(_snip_expand, ls.parser.parse_snippet( "", body, @@ -573,18 +638,18 @@ local function safe_choice_action(snip, ...) return session.current_nodes[vim.api.nvim_get_current_buf()] end end -local function change_choice(val, opts) + +local function _change_choice(val, opts) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] - assert(active_choice, "No active choiceNode") -- if the active choice exists current_node still does. local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] - local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node) - local new_active = no_region_check_wrap( - safe_choice_action, + local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) + + local new_active = safe_choice_action( active_choice.parent.snippet, active_choice.change_choice, active_choice, @@ -593,20 +658,26 @@ local function change_choice(val, opts) restore_data ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active - active_update_dependents() + _active_update_dependents() end -local function set_choice(choice_indx, opts) - local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] - local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node) +local function change_choice(val, opts) + api_do(_change_choice, val, opts) +end +local function _set_choice(choice_indx, opts) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] assert(active_choice, "No active choiceNode") + + local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] + + local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) + local choice = active_choice.choices[choice_indx] assert(choice, "Invalid Choice") - local new_active = no_region_check_wrap( - safe_choice_action, + + local new_active = safe_choice_action( active_choice.parent.snippet, active_choice.set_choice, active_choice, @@ -615,7 +686,11 @@ local function set_choice(choice_indx, opts) restore_data ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active - active_update_dependents() + _active_update_dependents() +end + +local function set_choice(choice_indx, opts) + api_do(_set_choice, choice_indx, opts) end local function get_current_choices() @@ -967,6 +1042,7 @@ ls = lazy_table({ get_active_snip = get_active_snip, choice_active = choice_active, change_choice = change_choice, + _set_choice = _set_choice, set_choice = set_choice, get_current_choices = get_current_choices, unlink_current = unlink_current, @@ -995,7 +1071,9 @@ ls = lazy_table({ extend_decorator = extend_decorator, log = require("luasnip.util.log"), activate_node = activate_node, - no_region_check_wrap = no_region_check_wrap + _api_do = api_do, + _api_enter = api_enter, + _api_leave = api_leave, }, ls_lazy) return ls diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 777c56070..64dccc1a0 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -284,7 +284,7 @@ function ChoiceNode:set_choice(choice, current_node, cursor_restore_data) -- and this choiceNode, then set the cursor. node_util.refocus(self, target_node) - node_util.restore_cursor_pos_relative(target_node, cursor_restore_data[target_node.parent.snippet]) + node_util.restore_cursor_pos_relative(target_node, cursor_restore_data[target_node.parent.snippet.node_store_id]) return target_node end diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 24b71ffb2..e6d5ef3d2 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -10,6 +10,7 @@ local feedkeys = require("luasnip.util.feedkeys") local snippet_string = require("luasnip.nodes.util.snippet_string") local str_util = require("luasnip.util.str") local log = require("luasnip.util.log").new("insertNode") +local session = require("luasnip.session") local function I(pos, static_text, opts) if not snippet_string.isinstance(static_text) then @@ -340,12 +341,19 @@ function InsertNode:get_snippetstring() return nil end + -- in order to accurately capture all the nodes inside eventual snippets, + -- call :store(), so these are up-to-date in the snippetString. + for _, snip in ipairs(self:child_snippets()) do + snip:store() + end + + local self_from, self_to = self.mark:pos_begin_end_raw() -- only do one get_text, and establish relative offsets partition this -- text. local ok, text = pcall(vim.api.nvim_buf_get_text, 0, self_from[1], self_from[2], self_to[1], self_to[2], {}) - local snippetstring = snippet_string.new() + local snippetstring = snippet_string.new(nil, {luasnip_changedtick = session.luasnip_changedtick}) if not ok then log.warn("Failure while getting text of insertNode: " .. text) @@ -379,13 +387,24 @@ function InsertNode:indent(indentstr) self.static_text:indent(indentstr) end +-- generate and cache text of this node when used as an argnode. function InsertNode:store() - for _, snip in ipairs(self:child_snippets()) do - snip:store() + if session.luasnip_changedtick and self.static_text.metadata and self.static_text.metadata.luasnip_changedtick == session.luasnip_changedtick then + -- stored data is up-to-date, just return the static text. + return end + + -- get_snippetstring calls store for all child-snippets. self.static_text = self:get_snippetstring() end +function InsertNode:argnode_text() + -- store caches its text, which is exactly what we want here! + self:store() + return self.static_text +end + + function InsertNode:put_initial(pos) self.static_text:put(pos) self.visible = true diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 1b12c6cc2..ffcebdc8e 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -256,20 +256,6 @@ local function get_args(node, get_text_func_name, static) -- visible (this second condition is to allow the docstring-generation -- to be improved by data provided after the expansion) if argnode and ((static and (argnode.static_visible or argnode.visible)) or (not static and argnode.visible)) then - if not static then - -- Don't store (aka call get_snippetstring) if this is a static - -- update (there will be no associated buffer-region!) and - -- don't store if the node is not visible. (Then there's - -- nothing to store anyway) - - -- now, store traverses the whole tree, and if one argnode includes - -- another we'd duplicate some work. - -- But I don't think there's a really good reason for doing - -- something like this (we already have all the data by capturing - -- the outer argnode), and even if it happens, it should occur only - -- rarely. - argnode:store() - end local argnode_text = argnode[get_text_func_name](argnode) -- can only occur with `get_text`. If one returns nil, the argnode -- isn't visible or some other error occured. Either way, return nil @@ -291,7 +277,7 @@ local function get_args(node, get_text_func_name, static) end function Node:get_args() - return get_args(self, "get_snippetstring", false) + return get_args(self, "argnode_text", false) end function Node:get_static_args() return get_args(self, "get_static_snippetstring", true) @@ -640,6 +626,10 @@ end -- those that don't. function Node:subtree_leave_entered() end +function Node:argnode_text() + return self:get_snippetstring() +end + return { Node = Node, focus_node = focus_node, diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 8a1ef1d31..f037a3f1d 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -1,4 +1,5 @@ local util = require("luasnip.util.util") +local str_util = require("luasnip.util.str") local tbl_util = require("luasnip.util.table") local ext_util = require("luasnip.util.ext_opts") local types = require("luasnip.util.types") @@ -863,12 +864,13 @@ local function str_args(args) end local store_id = 0 -local function store_cursor_node_relative(node) +local function store_cursor_node_relative(node, opts) local data = {} local snippet_current_node = node local cursor_state = feedkeys.last_state() + local store_ids = {} -- store for each snippet! -- the innermost snippet may be destroyed, and we would have to restore the @@ -878,40 +880,113 @@ local function store_cursor_node_relative(node) local snip_data = {} - snip_data.key = node.key - node.store_id = store_id + snip_data.key = snippet_current_node.key + snippet_current_node.store_id = store_id + snip.node_store_id = store_id + snip_data.store_id = store_id + snip_data.node = snippet_current_node - store_id = store_id + 1 + -- from low to high + table.insert(store_ids, store_id) snip_data.cursor_start_relative = - util.pos_offset(node.mark:get_endpoint(-1), cursor_state.pos) + util.pos_offset(snippet_current_node.mark:get_endpoint(-1), cursor_state.pos) snip_data.mode = cursor_state.mode if cursor_state.pos_v then - snip_data.selection_end_start_relative = util.pos_offset(node.mark:get_endpoint(-1), cursor_state.pos_v) + snip_data.selection_end_start_relative = util.pos_offset(snippet_current_node.mark:get_endpoint(-1), cursor_state.pos_v) + end + + if snippet_current_node.type == types.insertNode and opts.place_cursor_mark then + -- if the snippet_current_node is not an insertNode, the cursor + -- should always be exactly at the beginning if the node is entered + -- (which, btw, can only happen if a text or functionNode is + -- immediately nested inside a choiceNode), which means that + -- storing the cursor-position relative to the beginning of the + -- node is sufficient for restoring it in all usecases (now, a user + -- may have triggered the update while not in this position, but I + -- think it's fine to not restore the cursor 100% correctly in that + -- case. + -- + -- When the node is an insertNode, the cursor may be somewhere + -- inside the node, and while it will be restored correctly if the + -- text does not change, if the update inserts some characters or a + -- line at the beginning of the node (but still reaches + -- equilibrium), the cursor will have moved relative to the + -- immediately surrounding text. + -- + -- A solution to this is to simply place some kind if extmark (but + -- for regular strings, not for nvim-buffers) in the node (in the + -- text that is passed to some dynamicNode, to be precise), which + -- we then recover in the restore-function, and use to set the + -- cursor correctly :) + snippet_current_node:store() + + if snip_data.cursor_start_relative[1] >= 0 and snip_data.cursor_start_relative[2] >= 0 then + -- we also have this in static_text, but recomputing the text + -- exactly is rather expensive -> text is still in buffer, yank + -- it. + local str = snippet_current_node:get_text() + local pos_byte_offset = str_util.multiline_to_byte_offset(str, snip_data.cursor_start_relative) + if pos_byte_offset then + snippet_current_node.static_text:add_mark(store_id .. "pos", pos_byte_offset, false) + if snip_data.selection_end_start_relative and + snip_data.selection_end_start_relative[1] >= 0 and + snip_data.selection_end_start_relative[2] >= 0 then + local pos_v_byte_offset = str_util.multiline_to_byte_offset(str, snip_data.selection_end_start_relative) + if pos_v_byte_offset then + -- set rgrav of endpoint of selection true. + -- This means if the selection is replaced, it would still + -- be selected, which seems like a nice property. + snippet_current_node.static_text:add_mark(store_id .. "pos_v", pos_v_byte_offset, true) + end + end + end + end end - data[snip] = snip_data + data[snip] = snippet_current_node + data[store_id] = snip_data - snippet_current_node = snip:get_snippet().parent_node + snippet_current_node = snip.parent_node + + store_id = store_id + 1 end + data.store_ids = store_ids return data end local function restore_cursor_pos_relative(node, data) + local cursor_pos = data.cursor_start_relative + local cursor_pos_v = data.selection_end_start_relative + if node.type == types.insertNode then + local mark_pos = node.static_text:get_mark_pos(data.store_id .. "pos") + if mark_pos then + local str = node:get_text() + local mark_pos_offset = str_util.byte_to_multiline_offset(str, mark_pos) + cursor_pos = mark_pos_offset and mark_pos_offset or cursor_pos + + local mark_pos_v = node.static_text:get_mark_pos(data.store_id .. "pos_v") + if mark_pos_v then + local mark_pos_v_offset = str_util.byte_to_multiline_offset(str, mark_pos_v) + cursor_pos_v = mark_pos_v_offset + end + end + end + if data.mode == "i" then - feedkeys.insert_at(util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative)) + feedkeys.insert_at(util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)) elseif data.mode == "s" then -- is a selection => restore it. - local selection_from = util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative) - local selection_to = util.pos_from_offset(node.mark:get_endpoint(-1), data.selection_end_start_relative) + local selection_from = util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos) + local selection_to = util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos_v) feedkeys.select_range(selection_from, selection_to) else - feedkeys.move_to_normal(util.pos_from_offset(node.mark:get_endpoint(-1), data.cursor_start_relative)) + feedkeys.move_to_normal(util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)) end end diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 323c7f5eb..0c951bb42 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -370,9 +370,9 @@ local function _replace(self, replacements, snipstr_map) -- we already know that repl.to >= mark.pos. -- This means that the marker is inside the deleted region, and -- we have to somehow find a sensible new position. - -- For now, simply preserve the marks position if the new str - -- still covers the region, otherwise shift it to the beginning - -- or end of the newly inserted text, depending on rgrav. + + -- For now, shift the mark to the beginning or end of the newly + -- inserted text, depending on rgrav. mark.pos = (mark.rgrav and repl.to+1 or repl.from) - self_offset end -- in this case the replacement is completely behind the marks diff --git a/lua/luasnip/session/init.lua b/lua/luasnip/session/init.lua index ad1d6dfa4..3522e2c41 100644 --- a/lua/luasnip/session/init.lua +++ b/lua/luasnip/session/init.lua @@ -32,12 +32,18 @@ M.latest_load_ft = nil M.last_expand_snip = nil M.last_expand_opts = nil --- jump_active is set while luasnip moves the cursor, prevents --- (for example) updating dependents or deleting a snippet via --- exit_out_of_region while jumping. --- init with false, it will be set by (eg.) ls.jump(). +-- jump_active is set while luasnip moves the cursor (or is just generally +-- currently modifying the buffer), and prevents (for example) updating +-- dependents or deleting a snippet via exit_out_of_region while jumping (or +-- while any other state-modifying operation is being executed, and other +-- should therefore be prevented). init with false, it will be set by (eg.) +-- ls.jump(). M.jump_active = false +-- this is non-nil while a luasnip-api-call is active, and allows us to reuse +-- certain data that we just set without resorting to querying the buffer. +M.luasnip_changedtick = nil + -- initial value, might be overwritten immediately. -- No danger of overwriting user-config, since this has to be loaded to allow -- overwriting. diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 628da7a7d..80dd2ef20 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -154,6 +154,56 @@ function M.multiline_append(strmod, strappend) end end +-- turn a row+col-offset for a multiline-string (string[]) (where the column is +-- given in utf-codepoints and 0-based) into an offset (in bytes!, 1-based) for +-- the \n-concatenated version of that string. +function M.multiline_to_byte_offset(str, pos) + if pos[1] < 0 or pos[1]+1 > #str or pos[2] < 0 then + -- pos is trivially (row negative or beyond str, or col negative) + -- outside of str, can't represent position in str. + -- col-wise outside will be determined later, but we want this + -- precondition for following code. + return nil + end + + local byte_pos = 0 + for i = 1, pos[1] do + -- increase index by full lines, don't forget +1 for \n. + byte_pos = byte_pos + #str[i]+1 + end + + -- allow positions one beyond the last character for all lines (even the + -- last line). + local pos_line_str = str[pos[1]+1] .. "\n" + + if pos[2] >= #pos_line_str then + -- in this case, pos is outside of the multiline-region. + return nil + end + byte_pos = byte_pos + vim.str_byteindex(pos_line_str, pos[2]) + + -- 0- to 1-based columns + return byte_pos+1 +end + +-- inverse of multiline_to_byte_offset, 1-based byte to 0,0-based row,column, utf-aware. +function M.byte_to_multiline_offset(str, byte_pos) + if byte_pos < 0 then + return nil + end + + local byte_pos_so_far = 0 + for i, line in ipairs(str) do + local line_i_end = byte_pos_so_far + #line+1 + if byte_pos <= line_i_end then + -- byte located in this line, find utf-index. + local utf16_index = vim.str_utfindex(line .. "\n", byte_pos - byte_pos_so_far-1) + return {i-1, utf16_index} + end + byte_pos_so_far = line_i_end + end +end + -- string-operations implemented according to -- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415 -- such that they can be used for snippet-transformations in vscode-snippets. diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index 260199a43..3285231e4 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -61,7 +61,7 @@ describe("snippets_basic", function() ls.expand({ jump_into_func = function(snip) izero = snip.insert_nodes[0] - require("luasnip").no_region_check_wrap(izero.jump_into, izero, 1) + izero:jump_into(1) end }) ]]) diff --git a/tests/unit/str_spec.lua b/tests/unit/str_spec.lua index 5f90aa1db..53a397061 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -81,3 +81,72 @@ describe("str.multiline_substr", function() check("one last partial range", {"asdf", "qwer", "zxcv"}, {0,2}, {2,4}, {"df", "qwer", "zxcv"}) check("empty range", {"asdf", "qwer", "zxcv"}, {0,2}, {0,2}, {""}) end) + +describe("str.multiline_to_byte_offset", function() + -- apparently clear() needs to run before anything else... + ls_helpers.clear() + ls_helpers.exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE")) + + local function check(dscr, str, multiline_pos, byte_pos) + it(dscr, function() + assert.are.same(byte_pos, exec_lua([[ + local str, multiline_pos = ... + return require("luasnip.util.str").multiline_to_byte_offset(str, multiline_pos) + ]], str, multiline_pos)) + end) + end + local function check_is_nil(dscr, str, multiline_pos, byte_pos) + it(dscr, function() + assert(exec_lua([[ + local str, multiline_pos = ... + return require("luasnip.util.str").multiline_to_byte_offset(str, multiline_pos) == nil + ]], str, multiline_pos)) + end) + end + + check("single line begin", {"asdf"}, {0,0}, 1) + check("single line middle", {"asdf"}, {0,2}, 3) + check("single line end", {"asdf"}, {0,3}, 4) + check("single line, on \n", {"asdf"}, {0,4}, 5) + check_is_nil("single line, outside of range", {"asdf"}, {0,5}) + check("multiple lines", {"asdf", "qwer"}, {1,0}, 6) + check("multiple lines middle", {"asdf", "qwer"}, {1,3}, 9) + check_is_nil("multiple lines outside of range row", {"asdf", "qwer"}, {2,0}) + check("on linebreak", {"asdf", "qwer"}, {0,4}, 5) + check("on linebreak of last line", {"asdf", "qwer"}, {1,4}, 10) + check_is_nil("negative row", {"asdf", "qwer"}, {-1,0}) + check_is_nil("negative col", {"asdf", "qwer"}, {0,-2}) +end) + +describe("byte_to_multiline_offset", function() + -- apparently clear() needs to run before anything else... + ls_helpers.clear() + ls_helpers.exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE")) + + local function check(dscr, str, byte_pos, multiline_pos) + it(dscr, function() + assert.are.same(multiline_pos, exec_lua([[ + local str, byte_pos = ... + return require("luasnip.util.str").byte_to_multiline_offset(str, byte_pos) + ]], str, byte_pos)) + end) + end + local function check_is_nil(dscr, str, byte_pos, multiline_pos) + it(dscr, function() + assert(exec_lua([[ + local str, byte_pos = ... + return require("luasnip.util.str").byte_to_multiline_offset(str, byte_pos) == nil + ]], str, byte_pos)) + end) + end + + check("single line begin", {"asdf"}, 1, {0,0}) + check("single line middle", {"asdf"}, 3, {0,2}) + check("single line end", {"asdf"}, 4, {0,3}) + check("single line on linebreak", {"asdf"}, 5, {0,4}) + check("multiple lines", {"asdf", "qwer"}, 6, {1,0}) + check("multiple lines middle", {"asdf", "qwer"}, 9, {1,3}) + check("multiple lines middle linebreak", {"asdf", "qwer"}, 10, {1,4}) + check_is_nil("before string", {"asdf", "qwer"}, -1) + check_is_nil("multiple lines behind string", {"asdf", "qwer"}, 11) +end) From fe21f4926809d28d2e438a619ad23166138441e8 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:15:10 +0100 Subject: [PATCH 63/69] api_enter: only log an error when called recursively. --- lua/luasnip/init.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index c01a19e67..89ddba444 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -22,7 +22,14 @@ local log = require("luasnip.util.log").new("main") local luasnip_changedtick = 0 local function api_enter() session.jump_active = true - assert(session.luasnip_changedtick == nil) + if session.luasnip_changedtick ~= nil then + log.error([[ +api_enter called while luasnip_changedtick was non-nil. This +may be to a previous error, or due to unexpected control-flow. Check the +traceback and consider reporting this. Traceback: %s +]], debug.traceback()) + + end session.luasnip_changedtick = luasnip_changedtick luasnip_changedtick = luasnip_changedtick + 1 end From f19ebbd1d624ad9dc5fe80fc87ce42804595b222 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:15:59 +0100 Subject: [PATCH 64/69] feedkeys: ignore errors on asynchronous nvim_win_set_cursor. See extensive comment in commit. --- lua/luasnip/util/feedkeys.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 6ed6c1699..810e02b94 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -87,7 +87,24 @@ local function cursor_set_keys(pos, before) end end - return "lua vim.api.nvim_win_set_cursor(0,{" + -- since cursor-movements may happen asynchronously to other operations, + -- like deleting text, it's possible that we initiate a cursor movement, and + -- subsequently delete text, but the text is deleted before the cursor is + -- actually moved, which may (in the worst case) cause an error here. + -- This can be reproduced with the `session: position is restored correctly + -- after change_choice.`-test, which calls change_choice, in which + -- 1. active_update_dependents re-selects the currently active insertNode + -- 2. the immediately following change_choice removes the text associated + -- with the insertNode + -- -> the above, and an error here. + -- + -- I think a simple pcall is an appropriate solution, since removing the + -- text is very certainly done due to some other luasnip-operation, which + -- will also conclude with a cursor-movement. + -- Note that the cursor-store for that last movement may look into the + -- enqueued_cursor_state-variable, and thus has the correct position, even + -- if this move has not yet completed. + return "lua pcall(vim.api.nvim_win_set_cursor, 0,{" -- +1, win_set_cursor starts at 1. .. pos[1] + 1 .. "," From f13c479a60cf3f6b69b32ac0c6540c30afa97fd2 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:16:59 +0100 Subject: [PATCH 65/69] correctly restore self-dependent dynamicNode. By inserting the stored snip into the buffer, `get_args` during `update_restore` can find the argnode inside the snippet. --- lua/luasnip/nodes/dynamicNode.lua | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index d0ccc72a4..39631197c 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -7,6 +7,7 @@ local events = require("luasnip.util.events") local FunctionNode = require("luasnip.nodes.functionNode").FunctionNode local SnippetNode = require("luasnip.nodes.snippet").SN local extend_decorator = require("luasnip.util.extend_decorator") +local mark = require("luasnip.util.mark").mark local function D(pos, fn, args, opts) opts = opts or {} @@ -76,7 +77,32 @@ function DynamicNode:get_docstring() end -- DynamicNode's don't have static text, only set as visible. -function DynamicNode:put_initial(_) +function DynamicNode:put_initial(pos) + -- if we generated a snippet before, insert it into the buffer now. This + -- can happen if this dynamicNode was removed (eg. because of a + -- change_choice or an update to a dynamicNode), and is then reinserted due + -- to a restoreNode or snippetstring_args. + -- + -- This procedure is necessary to keep + if self.snip then + -- position might (will probably!!) still have changed, so update it + -- here too (as opposed to only in update). + self.snip:init_positions(self.snip_absolute_position) + self.snip:init_insert_positions(self.snip_absolute_insert_position) + + self.snip:make_args_absolute() + + self.snip:set_dependents() + self.snip:set_argnodes(self.parent.snippet.dependents_dict) + + local old_pos = vim.deepcopy(pos) + self.snip:put_initial(pos) + local mark_opts = vim.tbl_extend("keep", { + right_gravity = false, + end_right_gravity = false, + }, self.snip:get_passive_ext_opts()) + self.snip.mark = mark(old_pos, pos, mark_opts) + end self.visible = true end From a76f573f38b345eae17024308f53c71e6f1c6a49 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:18:09 +0100 Subject: [PATCH 66/69] change/set/select_choice: update current node before modifying choice. If we don't do this, the content of a choiceNode may not be restored correctly. (or, it will be restored correctly, but it won't be what the user saw before they called change/set/select_choice, which seems suboptimal). --- lua/luasnip/extras/select_choice.lua | 31 ++++++++++----- lua/luasnip/init.lua | 59 ++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/lua/luasnip/extras/select_choice.lua b/lua/luasnip/extras/select_choice.lua index 80af8c345..28cbc7a16 100644 --- a/lua/luasnip/extras/select_choice.lua +++ b/lua/luasnip/extras/select_choice.lua @@ -1,6 +1,7 @@ local session = require("luasnip.session") local ls = require("luasnip") local node_util = require("luasnip.nodes.util") +local feedkeys = require("luasnip.util.feedkeys") -- in this procedure, make sure that api_leave is called before -- set_choice_callback exits. @@ -10,9 +11,8 @@ local function set_choice_callback(data) ls._api_leave() return end - -- feed+immediately execute i to enter INSERT after vim.ui.input closes. - -- vim.api.nvim_feedkeys("i", "x", false) - ls._set_choice(indx, {cursor_restore_data = data}) + -- set_choice restores cursor from before. + ls._set_choice(indx, {cursor_restore_data = data, skip_update = true}) ls._api_leave() end end @@ -25,12 +25,25 @@ local function select_choice() local active = session.current_nodes[vim.api.nvim_get_current_buf()] ls._api_enter() - local restore_data = node_util.store_cursor_node_relative(active, {place_cursor_mark = false}) - vim.ui.select( - ls.get_current_choices(), - { kind = "luasnip" }, - set_choice_callback(restore_data) - ) + + ls._active_update_dependents() + + if not session.active_choice_nodes[vim.api.nvim_get_current_buf()] then + print("Active choice was removed while updating a dynamicNode.") + return + end + + local restore_data = node_util.store_cursor_node_relative(active, {place_cursor_mark = true}) + + -- make sure all movements are done, otherwise the movements may be put into + -- the select-dialog. + feedkeys.enqueue_action(function() + vim.ui.select( + ls.get_current_choices(), + { kind = "luasnip" }, + set_choice_callback(restore_data) + ) + end) end return select_choice diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 89ddba444..c83e64c83 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -172,7 +172,7 @@ end local function node_update_dependents_preserve_position(node, current, opts) -- set luasnip_changedtick so that static_text is preserved when possible. - local restore_data = node_util.store_cursor_node_relative(current, {place_cursor_mark = true}) + local restore_data = opts.cursor_restore_data or node_util.store_cursor_node_relative(current, {place_cursor_mark = true}) -- update all nodes that depend on this one. local ok, res = @@ -289,7 +289,7 @@ local function node_update_dependents_preserve_position(node, current, opts) end end -local function update_dependents(node) +local function update_dependents(node, opts) local active = session.current_nodes[vim.api.nvim_get_current_buf()] -- don't update if a jump/change_choice is in progress, or if we don't have -- an active node. @@ -297,7 +297,7 @@ local function update_dependents(node) local upd_res = node_update_dependents_preserve_position( node, active, - { no_move = false, restore_position = true } + { no_move = false, restore_position = true, cursor_restore_data = opts and opts.cursor_restore_data } ) if upd_res.new_current then upd_res.new_current:focus() @@ -305,8 +305,8 @@ local function update_dependents(node) end end end -local function _active_update_dependents() - update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()]) +local function _active_update_dependents(opts) + update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()], opts) end local function active_update_dependents() @@ -649,37 +649,59 @@ end local function _change_choice(val, opts) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] - assert(active_choice, "No active choiceNode") + + -- make sure we update completely, there may have been changes to the + -- buffer since the last update. + if not opts.skip_update then + assert(active_choice, "No active choiceNode") + + _active_update_dependents({ cursor_restore_data = opts.cursor_restore_data }) + + active_choice = + session.active_choice_nodes[vim.api.nvim_get_current_buf()] + if not active_choice then + print("Active choice was removed while updating a dynamicNode.") + return + end + end -- if the active choice exists current_node still does. local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] - local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) - local new_active = safe_choice_action( active_choice.parent.snippet, active_choice.change_choice, active_choice, val, session.current_nodes[vim.api.nvim_get_current_buf()], - restore_data + opts.skip_update and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active _active_update_dependents() end -local function change_choice(val, opts) - api_do(_change_choice, val, opts) +local function change_choice(val) + api_do(_change_choice, val, {}) end local function _set_choice(choice_indx, opts) local active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] - assert(active_choice, "No active choiceNode") - local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] + if not opts.skip_update then + assert(active_choice, "No active choiceNode") + + _active_update_dependents({ cursor_restore_data = opts.cursor_restore_data }) + + active_choice = + session.active_choice_nodes[vim.api.nvim_get_current_buf()] + if not active_choice then + print("Active choice was removed while updating a dynamicNode.") + return + end + end - local restore_data = opts and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) + local current_node = session.current_nodes[vim.api.nvim_get_current_buf()] local choice = active_choice.choices[choice_indx] assert(choice, "Invalid Choice") @@ -690,14 +712,16 @@ local function _set_choice(choice_indx, opts) active_choice, choice, current_node, - restore_data + -- if the update was skipped, we have to use the cursor_restore_data + -- here. + opts.skip_update and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active _active_update_dependents() end -local function set_choice(choice_indx, opts) - api_do(_set_choice, choice_indx, opts) +local function set_choice(choice_indx) + api_do(_set_choice, choice_indx, {}) end local function get_current_choices() @@ -1055,6 +1079,7 @@ ls = lazy_table({ unlink_current = unlink_current, lsp_expand = lsp_expand, active_update_dependents = active_update_dependents, + _active_update_dependents = _active_update_dependents, available = available, exit_out_of_region = exit_out_of_region, load_snippet_docstrings = load_snippet_docstrings, From d066147f426577f5c582089cc58cf2e67edc3a61 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:20:02 +0100 Subject: [PATCH 67/69] add a few tests for previous changes. --- tests/integration/choice_spec.lua | 136 ++++++++++++++++++++++++++++- tests/integration/dynamic_spec.lua | 32 +++++++ tests/integration/restore_spec.lua | 2 +- 3 files changed, 167 insertions(+), 3 deletions(-) diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index 3b4d28d1d..f0b6bb755 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -523,14 +523,146 @@ screen:expect({ {2:-- SELECT --} | ]] }) + feed("aa") + -- simulate vim.ui.select that modifies the cursor. + -- Can happen in the wild with plugins like dressing.nvim (although + -- those usually just leave INSERT), and we would like to prevent it. + exec_lua[[ + vim.ui.select = function(_,_,cb) + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes( + "", + true, + false, + true + ), + "nix", + true) + + cb(nil, 2) + end + ]] -- re-selecting correctly highlights text again (test by editing so the test does not pass immediately, without any changes!) - feed("lua require('luasnip.extras.select_choice')()2val") + exec_lua("require('luasnip.extras.select_choice')()") screen:expect({ grid = [[ - for val^ in do | + for a^a in do | | {2:-- INSERT --} | ]] +}) + end) + + it("updates the active node before changing choice.", function() + exec_lua[[ + ls.setup({ + link_children = true + }) + ls.snip_expand(s("trig", { + t":", + c(1, { + {r(1, "key", d(1, function(args) + if not args[1] then + return sn(nil, {i(1, "aa", {key = "i"})}) + else + return sn(nil, {i(1, "cc"), i(2, args[1]:gsub("a", "ee"), {key = "i"})}) + end + end, { opt(k("i")) }, {snippetstring_args = true}))}, + {t".", r(1, "key"), t"."} + }, {restore_cursor = true}), + t":" + })) + ]] + exec_lua"ls.jump(1)" + feed("i aa ") +screen:expect({ + grid = [[ + :ccee a^a ee: | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + -- if we wouldn't update before the change_choice, the last_args of the + -- restored dynamicNode would not fit its current content, and we'd + -- lose the text inserted until now due to the update (as opposed to + -- a proper restore of dynamicNode.snip, which should occur in a + -- restoreNode). + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + :.ccee ee^ee ee.: | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + exec_lua"ls.set_choice(2)" +screen:expect({ unchanged = true }) + + -- test some more wild stuff, just because. + feed(" ") + exec_lua[[ + ls.snip_expand(s("trig", { + t":", + c(1, { + {r(1, "key", d(1, function(args) + if not args[1] then + return sn(nil, {i(1, "aa", {key = "i"})}) + else + return sn(nil, {i(1, "cc"), i(2, args[1]:gsub("a", "ee"), {key = "i"})}) + end + end, { opt(k("i")) }, {snippetstring_args = true}))}, + {t".", r(1, "key"), t"."} + }, {restore_cursor = true}), + t":" + })) + ]] + +screen:expect({ + grid = [[ + :.ccee e :^c{3:c}eeee:eee ee.: | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1)" + feed("i aa ") + exec_lua"ls.set_choice(2)" +screen:expect({ + grid = [[ + :.ccee e :.ccee ee^ee ee.:eee ee.: | + {0:~ }| + {2:-- INSERT --} | + ]] +}) + + -- reselect outer choiceNode + exec_lua"ls.jump(-1)" + exec_lua"ls.jump(-1)" + exec_lua"ls.jump(-1)" + exec_lua"ls.jump(1)" +screen:expect({ + grid = [[ + :.cc^e{3:e e :.ccee eeee ee.:eee ee}.: | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.change_choice(1)" +screen:expect({ + grid = [[ + :cc^e{3:e e :.ccee eeee ee.:eee ee}: | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + exec_lua"ls.jump(1)" + exec_lua"ls.jump(1)" +screen:expect({ + grid = [[ + :ccee e :.cc^e{3:e eeee ee}.:eee ee: | + {0:~ }| + {2:-- SELECT --} | + ]] }) end) end) diff --git a/tests/integration/dynamic_spec.lua b/tests/integration/dynamic_spec.lua index afd2d3282..900385eae 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -427,4 +427,36 @@ screen:expect({ ]] }) end) + + it("cursor-position is moved with text-manipulations.", function() + exec_lua[[ + ls.snip_expand(s("trig", { + d(1, function(args) + if not args[1] then + return sn(nil, {i(1, "asdf", {key = "ins"})}) + else + return sn(nil, {i(1, args[1]:gsub("a", "ee"), {key = "ins"})}) + end + end, {opt(k("ins"))}, {snippetstring_args = true}) + })) + ]] + +screen:expect({ + grid = [[ + ^e{3:esdf} | + {0:~ }| + {2:-- SELECT --} | + ]] +}) + feed("aaaaaa") +screen:expect({ + grid = [[ + eeeeee^eeeeee | + {0:~ }| + | + ]] +}) + end) + + it("") end) diff --git a/tests/integration/restore_spec.lua b/tests/integration/restore_spec.lua index 9a77cd5cf..ac2f94007 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -462,7 +462,7 @@ screen:expect({ end) -- make sure store and update_restore propagate. - it("correctly restores snippets (3).", function() + it("correctly restores snippets (4).", function() exec_lua([[ ls.setup({link_children = true}) From 1aa841e4db7bac4e67b06297599b27b2fccd5d13 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 11:21:02 +0000 Subject: [PATCH 68/69] Auto generate docs --- doc/luasnip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index d90d691ae..a233fb0c6 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NVIM v0.8.0 Last change: 2024 November 05 +*luasnip.txt* For NVIM v0.8.0 Last change: 2024 November 14 ============================================================================== Table of Contents *luasnip-table-of-contents* From cabdbfaae6921a3707b4e70762751bf57d6f6d90 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 11:21:04 +0000 Subject: [PATCH 69/69] Format with stylua --- lua/luasnip/config.lua | 17 +- lua/luasnip/extras/select_choice.lua | 7 +- lua/luasnip/init.lua | 85 ++++++-- lua/luasnip/nodes/choiceNode.lua | 12 +- lua/luasnip/nodes/dynamicNode.lua | 30 ++- lua/luasnip/nodes/insertNode.lua | 61 ++++-- lua/luasnip/nodes/node.lua | 19 +- lua/luasnip/nodes/util.lua | 95 ++++++--- lua/luasnip/nodes/util/snippet_string.lua | 190 +++++++++++------- lua/luasnip/util/feedkeys.lua | 18 +- lua/luasnip/util/str.lua | 19 +- lua/luasnip/util/util.lua | 9 +- tests/integration/choice_spec.lua | 234 +++++++++++----------- tests/integration/dynamic_spec.lua | 65 +++--- tests/integration/restore_spec.lua | 127 ++++++------ tests/integration/session_spec.lua | 12 +- tests/unit/str_spec.lua | 130 ++++++++---- 17 files changed, 685 insertions(+), 445 deletions(-) diff --git a/lua/luasnip/config.lua b/lua/luasnip/config.lua index 41518b050..c9eb9f6df 100644 --- a/lua/luasnip/config.lua +++ b/lua/luasnip/config.lua @@ -101,17 +101,14 @@ c = { require("luasnip").unlink_current_if_deleted ) end - ls_autocmd( - session.config.update_events, - function() - -- don't update due to events if an update due to luasnip is pending anyway. - -- (Also, this would be bad because luasnip may not be in an - -- consistent state whenever an autocommand is triggered) - if not session.jump_active then - require("luasnip").active_update_dependents() - end + ls_autocmd(session.config.update_events, function() + -- don't update due to events if an update due to luasnip is pending anyway. + -- (Also, this would be bad because luasnip may not be in an + -- consistent state whenever an autocommand is triggered) + if not session.jump_active then + require("luasnip").active_update_dependents() end - ) + end) if session.config.region_check_events ~= nil then ls_autocmd(session.config.region_check_events, function() require("luasnip").exit_out_of_region( diff --git a/lua/luasnip/extras/select_choice.lua b/lua/luasnip/extras/select_choice.lua index 28cbc7a16..0b969fb63 100644 --- a/lua/luasnip/extras/select_choice.lua +++ b/lua/luasnip/extras/select_choice.lua @@ -12,7 +12,7 @@ local function set_choice_callback(data) return end -- set_choice restores cursor from before. - ls._set_choice(indx, {cursor_restore_data = data, skip_update = true}) + ls._set_choice(indx, { cursor_restore_data = data, skip_update = true }) ls._api_leave() end end @@ -33,7 +33,10 @@ local function select_choice() return end - local restore_data = node_util.store_cursor_node_relative(active, {place_cursor_mark = true}) + local restore_data = node_util.store_cursor_node_relative( + active, + { place_cursor_mark = true } + ) -- make sure all movements are done, otherwise the movements may be put into -- the select-dialog. diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index c83e64c83..11b24fdb5 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -23,12 +23,14 @@ local luasnip_changedtick = 0 local function api_enter() session.jump_active = true if session.luasnip_changedtick ~= nil then - log.error([[ + log.error( + [[ api_enter called while luasnip_changedtick was non-nil. This may be to a previous error, or due to unexpected control-flow. Check the traceback and consider reporting this. Traceback: %s -]], debug.traceback()) - +]], + debug.traceback() + ) end session.luasnip_changedtick = luasnip_changedtick luasnip_changedtick = luasnip_changedtick + 1 @@ -52,7 +54,6 @@ local function api_do(fn, ...) return fn_res end - local function get_active_snip() local node = session.current_nodes[vim.api.nvim_get_current_buf()] if not node then @@ -172,7 +173,11 @@ end local function node_update_dependents_preserve_position(node, current, opts) -- set luasnip_changedtick so that static_text is preserved when possible. - local restore_data = opts.cursor_restore_data or node_util.store_cursor_node_relative(current, {place_cursor_mark = true}) + local restore_data = opts.cursor_restore_data + or node_util.store_cursor_node_relative( + current, + { place_cursor_mark = true } + ) -- update all nodes that depend on this one. local ok, res = @@ -198,7 +203,10 @@ local function node_update_dependents_preserve_position(node, current, opts) if not opts.no_move and opts.restore_position then -- node is visible: restore position. local active_snippet = current:get_snippet() - node_util.restore_cursor_pos_relative(current, restore_data[active_snippet.node_store_id]) + node_util.restore_cursor_pos_relative( + current, + restore_data[active_snippet.node_store_id] + ) end return { jump_done = false, new_current = current } @@ -218,7 +226,8 @@ local function node_update_dependents_preserve_position(node, current, opts) -- have found first visible snippet => look for visible dynamicNode, -- starting from which we can try to find a new active node. - local node_parent = restore_data[active_snippet.node_store_id].node.parent + local node_parent = + restore_data[active_snippet.node_store_id].node.parent -- find visible dynamicNode that contained the (now-inactive) insertNode. -- since the node was no longer visible after an update, it must have @@ -243,22 +252,32 @@ local function node_update_dependents_preserve_position(node, current, opts) -- any snippet we encounter here was generated before, and -- if sd_node has the correct key, its snippet has a -- node_store_id that corresponds to it. - local snip_node_store_id = sd_node.parent.snippet.node_store_id + local snip_node_store_id = + sd_node.parent.snippet.node_store_id -- make sure that the key we found belongs to this -- snippets' active node. -- Also use the first valid node, and not the second one. -- Doesn't really matter (ambiguous keys -> undefined -- behaviour), but we should just use the first one, as -- that seems more like what would be expected. - if snip_node_store_id and restore_data[snip_node_store_id] and sd_node.key == restore_data[snip_node_store_id].key and not found_nodes[snip_node_store_id] then + if + snip_node_store_id + and restore_data[snip_node_store_id] + and sd_node.key == restore_data[snip_node_store_id].key + and not found_nodes[snip_node_store_id] + then found_nodes[snip_node_store_id] = sd_node end - elseif sd_node.store_id and restore_data[sd_node.store_id] and not found_nodes[sd_node.store_id] then + elseif + sd_node.store_id + and restore_data[sd_node.store_id] + and not found_nodes[sd_node.store_id] + then found_nodes[sd_node.store_id] = sd_node end end, - post=util.nop, - do_child_snippets=true + post = util.nop, + do_child_snippets = true, }) local new_current @@ -274,7 +293,10 @@ local function node_update_dependents_preserve_position(node, current, opts) if not opts.no_move and opts.restore_position then -- node is visible: restore position - node_util.restore_cursor_pos_relative(new_current, restore_data[new_current.parent.snippet.node_store_id]) + node_util.restore_cursor_pos_relative( + new_current, + restore_data[new_current.parent.snippet.node_store_id] + ) end return { jump_done = false, new_current = new_current } @@ -297,16 +319,24 @@ local function update_dependents(node, opts) local upd_res = node_update_dependents_preserve_position( node, active, - { no_move = false, restore_position = true, cursor_restore_data = opts and opts.cursor_restore_data } + { + no_move = false, + restore_position = true, + cursor_restore_data = opts and opts.cursor_restore_data, + } ) if upd_res.new_current then upd_res.new_current:focus() - session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_current + session.current_nodes[vim.api.nvim_get_current_buf()] = + upd_res.new_current end end end local function _active_update_dependents(opts) - update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()], opts) + update_dependents( + session.current_nodes[vim.api.nvim_get_current_buf()], + opts + ) end local function active_update_dependents() @@ -612,7 +642,8 @@ end local function lsp_expand(body, opts) -- expand snippet as-is. - api_do(_snip_expand, + api_do( + _snip_expand, ls.parser.parse_snippet( "", body, @@ -655,7 +686,9 @@ local function _change_choice(val, opts) if not opts.skip_update then assert(active_choice, "No active choiceNode") - _active_update_dependents({ cursor_restore_data = opts.cursor_restore_data }) + _active_update_dependents({ + cursor_restore_data = opts.cursor_restore_data, + }) active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] @@ -674,7 +707,11 @@ local function _change_choice(val, opts) active_choice, val, session.current_nodes[vim.api.nvim_get_current_buf()], - opts.skip_update and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) + opts.skip_update and opts.cursor_restore_data + or node_util.store_cursor_node_relative( + current_node, + { place_cursor_mark = false } + ) ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active _active_update_dependents() @@ -691,7 +728,9 @@ local function _set_choice(choice_indx, opts) if not opts.skip_update then assert(active_choice, "No active choiceNode") - _active_update_dependents({ cursor_restore_data = opts.cursor_restore_data }) + _active_update_dependents({ + cursor_restore_data = opts.cursor_restore_data, + }) active_choice = session.active_choice_nodes[vim.api.nvim_get_current_buf()] @@ -714,7 +753,11 @@ local function _set_choice(choice_indx, opts) current_node, -- if the update was skipped, we have to use the cursor_restore_data -- here. - opts.skip_update and opts.cursor_restore_data or node_util.store_cursor_node_relative(current_node, {place_cursor_mark = false}) + opts.skip_update and opts.cursor_restore_data + or node_util.store_cursor_node_relative( + current_node, + { place_cursor_mark = false } + ) ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active _active_update_dependents() diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 64dccc1a0..57a7e1054 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -169,10 +169,7 @@ function ChoiceNode:get_static_text() end function ChoiceNode:get_docstring() - return util.string_wrap( - self.choices[1]:get_docstring(), - self.pos - ) + return util.string_wrap(self.choices[1]:get_docstring(), self.pos) end function ChoiceNode:jump_into(dir, no_move, dry_run) @@ -276,7 +273,7 @@ function ChoiceNode:set_choice(choice, current_node, cursor_restore_data) if self.restore_cursor then local target_node = self:find_node(function(test_node) return test_node.change_choice_id == change_choice_id - end, {find_in_child_snippets = true}) + end, { find_in_child_snippets = true }) if target_node then -- the node that the cursor was in when changeChoice was called @@ -284,7 +281,10 @@ function ChoiceNode:set_choice(choice, current_node, cursor_restore_data) -- and this choiceNode, then set the cursor. node_util.refocus(self, target_node) - node_util.restore_cursor_pos_relative(target_node, cursor_restore_data[target_node.parent.snippet.node_store_id]) + node_util.restore_cursor_pos_relative( + target_node, + cursor_restore_data[target_node.parent.snippet.node_store_id] + ) return target_node end diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 39631197c..3543b8342 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -83,7 +83,7 @@ function DynamicNode:put_initial(pos) -- change_choice or an update to a dynamicNode), and is then reinserted due -- to a restoreNode or snippetstring_args. -- - -- This procedure is necessary to keep + -- This procedure is necessary to keep if self.snip then -- position might (will probably!!) still have changed, so update it -- here too (as opposed to only in update). @@ -194,7 +194,12 @@ function DynamicNode:update() tmp = SnippetNode(nil, {}) else -- also enter node here. - tmp = self.fn(effective_args, self.parent, nil, unpack(self.user_args)) + tmp = self.fn( + effective_args, + self.parent, + nil, + unpack(self.user_args) + ) end end @@ -296,8 +301,13 @@ function DynamicNode:update_static() tmp = SnippetNode(nil, {}) else -- also enter node here. - ok, tmp = - pcall(self.fn, effective_args, self.parent, nil, unpack(self.user_args)) + ok, tmp = pcall( + self.fn, + effective_args, + self.parent, + nil, + unpack(self.user_args) + ) end end if not ok then @@ -349,7 +359,11 @@ function DynamicNode:update_static() tmp:update_static() -- updates own dependents. - self:update_dependents_static({ own = true, parents = true, children = true }) + self:update_dependents_static({ + own = true, + parents = true, + children = true, + }) end function DynamicNode:exit() @@ -384,7 +398,11 @@ function DynamicNode:update_restore() local str_args = node_util.str_args(args) -- only insert snip if it is not currently visible! - if self.snip and not self.snip.visible and vim.deep_equal(str_args, self.last_args) then + if + self.snip + and not self.snip.visible + and vim.deep_equal(str_args, self.last_args) + then local tmp = self.snip -- position might (will probably!!) still have changed, so update it diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index e6d5ef3d2..266e3783d 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -347,13 +347,23 @@ function InsertNode:get_snippetstring() snip:store() end - local self_from, self_to = self.mark:pos_begin_end_raw() -- only do one get_text, and establish relative offsets partition this -- text. - local ok, text = pcall(vim.api.nvim_buf_get_text, 0, self_from[1], self_from[2], self_to[1], self_to[2], {}) + local ok, text = pcall( + vim.api.nvim_buf_get_text, + 0, + self_from[1], + self_from[2], + self_to[1], + self_to[2], + {} + ) - local snippetstring = snippet_string.new(nil, {luasnip_changedtick = session.luasnip_changedtick}) + local snippetstring = snippet_string.new( + nil, + { luasnip_changedtick = session.luasnip_changedtick } + ) if not ok then log.warn("Failure while getting text of insertNode: " .. text) @@ -361,17 +371,32 @@ function InsertNode:get_snippetstring() return snippetstring end - local current = {0,0} + local current = { 0, 0 } for _, snip in ipairs(self:child_snippets()) do local snip_from, snip_to = snip.mark:pos_begin_end_raw() local snip_from_base_rel = util.pos_offset(self_from, snip_from) - local snip_to_base_rel = util.pos_offset(self_from, snip_to) - - snippetstring:append_text(str_util.multiline_substr(text, current, snip_from_base_rel)) - snippetstring:append_snip(snip, str_util.multiline_substr(text, snip_from_base_rel, snip_to_base_rel)) + local snip_to_base_rel = util.pos_offset(self_from, snip_to) + + snippetstring:append_text( + str_util.multiline_substr(text, current, snip_from_base_rel) + ) + snippetstring:append_snip( + snip, + str_util.multiline_substr( + text, + snip_from_base_rel, + snip_to_base_rel + ) + ) current = snip_to_base_rel end - snippetstring:append_text(str_util.multiline_substr(text, current, util.pos_offset(self_from, self_to))) + snippetstring:append_text( + str_util.multiline_substr( + text, + current, + util.pos_offset(self_from, self_to) + ) + ) return snippetstring end @@ -389,7 +414,12 @@ end -- generate and cache text of this node when used as an argnode. function InsertNode:store() - if session.luasnip_changedtick and self.static_text.metadata and self.static_text.metadata.luasnip_changedtick == session.luasnip_changedtick then + if + session.luasnip_changedtick + and self.static_text.metadata + and self.static_text.metadata.luasnip_changedtick + == session.luasnip_changedtick + then -- stored data is up-to-date, just return the static text. return end @@ -404,7 +434,6 @@ function InsertNode:argnode_text() return self.static_text end - function InsertNode:put_initial(pos) self.static_text:put(pos) self.visible = true @@ -416,12 +445,18 @@ function InsertNode:put_initial(pos) -- index. true, -- don't enter snippets, we want to find the position of this node. - node_util.binarysearch_preference.outside) + node_util.binarysearch_preference.outside + ) for snip in self.static_text:iter_snippets() do -- don't have to pass a current_node, we don't need it since we can -- certainly link the snippet into this insertNode. - snip:insert_into_jumplist(nil, self, self.parent.snippet.child_snippets, child_snippet_idx) + snip:insert_into_jumplist( + nil, + self, + self.parent.snippet.child_snippets, + child_snippet_idx + ) child_snippet_idx = child_snippet_idx + 1 end diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index ffcebdc8e..d0aa5c05a 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -255,7 +255,13 @@ local function get_args(node, get_text_func_name, static) -- * we are doing a static update and it is not static_visible or -- visible (this second condition is to allow the docstring-generation -- to be improved by data provided after the expansion) - if argnode and ((static and (argnode.static_visible or argnode.visible)) or (not static and argnode.visible)) then + if + argnode + and ( + (static and (argnode.static_visible or argnode.visible)) + or (not static and argnode.visible) + ) + then local argnode_text = argnode[get_text_func_name](argnode) -- can only occur with `get_text`. If one returns nil, the argnode -- isn't visible or some other error occured. Either way, return nil @@ -562,17 +568,12 @@ end function Node:linkable() -- linkable if insert or exitNode. - return vim.tbl_contains( - { types.insertNode, types.exitNode }, - self.type - ) + return vim.tbl_contains({ types.insertNode, types.exitNode }, self.type) end function Node:interactive() -- interactive if immediately inside choiceNode. - return vim.tbl_contains( - { types.insertNode, types.exitNode }, - self.type - ) or self.choice ~= nil + return vim.tbl_contains({ types.insertNode, types.exitNode }, self.type) + or self.choice ~= nil end function Node:leaf() return vim.tbl_contains( diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index f037a3f1d..82407dc94 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -186,10 +186,7 @@ end local function linkable_node(node) -- node.type has to be one of insertNode, exitNode. - return vim.tbl_contains( - { types.insertNode, types.exitNode }, - node.type - ) + return vim.tbl_contains({ types.insertNode, types.exitNode }, node.type) end -- mainly used internally, by binarysearch_pos. @@ -199,10 +196,7 @@ end -- feel appropriate (higher runtime), most cases should be served well by this -- heuristic. local function non_linkable_node(node) - return vim.tbl_contains( - { types.textNode, types.functionNode }, - node.type - ) + return vim.tbl_contains({ types.textNode, types.functionNode }, node.type) end -- return whether a node is certainly (not) interactive. -- Coincindentially, the same nodes as (non-)linkable ones, but since there is a @@ -858,9 +852,11 @@ local function collect_dependents(node, which, static) end local function str_args(args) - return args and vim.tbl_map(function(arg) - return snippet_string.isinstance(arg) and vim.split(arg:str(), "\n") or arg - end, args) + return args + and vim.tbl_map(function(arg) + return snippet_string.isinstance(arg) and vim.split(arg:str(), "\n") + or arg + end, args) end local store_id = 0 @@ -891,16 +887,24 @@ local function store_cursor_node_relative(node, opts) -- from low to high table.insert(store_ids, store_id) - snip_data.cursor_start_relative = - util.pos_offset(snippet_current_node.mark:get_endpoint(-1), cursor_state.pos) + snip_data.cursor_start_relative = util.pos_offset( + snippet_current_node.mark:get_endpoint(-1), + cursor_state.pos + ) snip_data.mode = cursor_state.mode if cursor_state.pos_v then - snip_data.selection_end_start_relative = util.pos_offset(snippet_current_node.mark:get_endpoint(-1), cursor_state.pos_v) + snip_data.selection_end_start_relative = util.pos_offset( + snippet_current_node.mark:get_endpoint(-1), + cursor_state.pos_v + ) end - if snippet_current_node.type == types.insertNode and opts.place_cursor_mark then + if + snippet_current_node.type == types.insertNode + and opts.place_cursor_mark + then -- if the snippet_current_node is not an insertNode, the cursor -- should always be exactly at the beginning if the node is entered -- (which, btw, can only happen if a text or functionNode is @@ -925,23 +929,43 @@ local function store_cursor_node_relative(node, opts) -- cursor correctly :) snippet_current_node:store() - if snip_data.cursor_start_relative[1] >= 0 and snip_data.cursor_start_relative[2] >= 0 then + if + snip_data.cursor_start_relative[1] >= 0 + and snip_data.cursor_start_relative[2] >= 0 + then -- we also have this in static_text, but recomputing the text -- exactly is rather expensive -> text is still in buffer, yank -- it. local str = snippet_current_node:get_text() - local pos_byte_offset = str_util.multiline_to_byte_offset(str, snip_data.cursor_start_relative) + local pos_byte_offset = str_util.multiline_to_byte_offset( + str, + snip_data.cursor_start_relative + ) if pos_byte_offset then - snippet_current_node.static_text:add_mark(store_id .. "pos", pos_byte_offset, false) - if snip_data.selection_end_start_relative and - snip_data.selection_end_start_relative[1] >= 0 and - snip_data.selection_end_start_relative[2] >= 0 then - local pos_v_byte_offset = str_util.multiline_to_byte_offset(str, snip_data.selection_end_start_relative) + snippet_current_node.static_text:add_mark( + store_id .. "pos", + pos_byte_offset, + false + ) + if + snip_data.selection_end_start_relative + and snip_data.selection_end_start_relative[1] >= 0 + and snip_data.selection_end_start_relative[2] >= 0 + then + local pos_v_byte_offset = + str_util.multiline_to_byte_offset( + str, + snip_data.selection_end_start_relative + ) if pos_v_byte_offset then -- set rgrav of endpoint of selection true. -- This means if the selection is replaced, it would still -- be selected, which seems like a nice property. - snippet_current_node.static_text:add_mark(store_id .. "pos_v", pos_v_byte_offset, true) + snippet_current_node.static_text:add_mark( + store_id .. "pos_v", + pos_v_byte_offset, + true + ) end end end @@ -967,26 +991,35 @@ local function restore_cursor_pos_relative(node, data) local mark_pos = node.static_text:get_mark_pos(data.store_id .. "pos") if mark_pos then local str = node:get_text() - local mark_pos_offset = str_util.byte_to_multiline_offset(str, mark_pos) + local mark_pos_offset = + str_util.byte_to_multiline_offset(str, mark_pos) cursor_pos = mark_pos_offset and mark_pos_offset or cursor_pos - local mark_pos_v = node.static_text:get_mark_pos(data.store_id .. "pos_v") + local mark_pos_v = + node.static_text:get_mark_pos(data.store_id .. "pos_v") if mark_pos_v then - local mark_pos_v_offset = str_util.byte_to_multiline_offset(str, mark_pos_v) + local mark_pos_v_offset = + str_util.byte_to_multiline_offset(str, mark_pos_v) cursor_pos_v = mark_pos_v_offset end end end if data.mode == "i" then - feedkeys.insert_at(util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)) + feedkeys.insert_at( + util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos) + ) elseif data.mode == "s" then -- is a selection => restore it. - local selection_from = util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos) - local selection_to = util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos_v) + local selection_from = + util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos) + local selection_to = + util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos_v) feedkeys.select_range(selection_from, selection_to) else - feedkeys.move_to_normal(util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos)) + feedkeys.move_to_normal( + util.pos_from_offset(node.mark:get_endpoint(-1), cursor_pos) + ) end end @@ -1017,5 +1050,5 @@ return { node_subtree_do = node_subtree_do, str_args = str_args, store_cursor_node_relative = store_cursor_node_relative, - restore_cursor_pos_relative = restore_cursor_pos_relative + restore_cursor_pos_relative = restore_cursor_pos_relative, } diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 0c951bb42..57ad9aba6 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -15,7 +15,11 @@ local M = {} ---@param initial_str string[]?, optional initial multiline string. ---@return SnippetString function M.new(initial_str, metadata) - local o = {initial_str and table.concat(initial_str, "\n"), marks = {}, metadata = metadata} + local o = { + initial_str and table.concat(initial_str, "\n"), + marks = {}, + metadata = metadata, + } return setmetatable(o, SnippetString_mt) end @@ -24,7 +28,7 @@ function M.isinstance(o) end function SnippetString:append_snip(snip) - table.insert(self, {snip = snip}) + table.insert(self, { snip = snip }) end function SnippetString:append_text(str) table.insert(self, table.concat(str, "\n")) @@ -47,14 +51,19 @@ local function gen_snipstr_map(self, map, from_offset) pre = function(node) if node.static_text then if M.isinstance(node.static_text) then - local nested_str = gen_snipstr_map(node.static_text, map, from_offset + #str + #snip_str) + local nested_str = gen_snipstr_map( + node.static_text, + map, + from_offset + #str + #snip_str + ) snip_str = snip_str .. nested_str else - snip_str = snip_str .. table.concat(node.static_text, "\n") + snip_str = snip_str + .. table.concat(node.static_text, "\n") end end end, - post = util.nop + post = util.nop, }) map[v.snip] = snip_str str = str .. snip_str @@ -101,11 +110,11 @@ function SnippetString:iter_snippets() local i = 1 return function() -- find the next snippet. - while self[i] and (not self[i].snip) do - i = i+1 + while self[i] and not self[i].snip do + i = i + 1 end local res = self[i] and self[i].snip - i = i+1 + i = i + 1 return res end end @@ -123,47 +132,49 @@ end function SnippetString:copy() -- on 0.7 vim.deepcopy does not behave correctly on snippets => have to manually copy. - return setmetatable(vim.tbl_map(function(snipstr_or_str) - if snipstr_or_str.snip then - local snip = snipstr_or_str.snip - - -- remove associations with objects beyond this snippet. - -- This is so we can easily deepcopy it without copying too much data. - -- We could also do this copy in - local prevprev = snip.prev.prev - local i0next = snip.insert_nodes[0].next - local parentnode = snip.parent_node - - snip.prev.prev = nil - snip.insert_nodes[0].next = nil - snip.parent_node = nil - - local snipcop = snip:copy() - - snip.prev.prev = prevprev - snip.insert_nodes[0].next = i0next - snip.parent_node = parentnode - - - -- bring into inactive mode, so that we will jump into it correctly when it - -- is expanded again. - snipcop:subtree_do({ - pre = function(node) - node.mark:invalidate() - end, - post = util.nop, - do_child_snippets = true - }) - -- snippet may have been active (for example if captured as an - -- argnode), so finally exit here (so we can put_initial it again!) - snipcop:exit() - - return {snip = snipcop} - else - -- handles raw strings and marks and metadata - return vim.deepcopy(snipstr_or_str) - end - end, self), SnippetString_mt) + return setmetatable( + vim.tbl_map(function(snipstr_or_str) + if snipstr_or_str.snip then + local snip = snipstr_or_str.snip + + -- remove associations with objects beyond this snippet. + -- This is so we can easily deepcopy it without copying too much data. + -- We could also do this copy in + local prevprev = snip.prev.prev + local i0next = snip.insert_nodes[0].next + local parentnode = snip.parent_node + + snip.prev.prev = nil + snip.insert_nodes[0].next = nil + snip.parent_node = nil + + local snipcop = snip:copy() + + snip.prev.prev = prevprev + snip.insert_nodes[0].next = i0next + snip.parent_node = parentnode + + -- bring into inactive mode, so that we will jump into it correctly when it + -- is expanded again. + snipcop:subtree_do({ + pre = function(node) + node.mark:invalidate() + end, + post = util.nop, + do_child_snippets = true, + }) + -- snippet may have been active (for example if captured as an + -- argnode), so finally exit here (so we can put_initial it again!) + snipcop:exit() + + return { snip = snipcop } + else + -- handles raw strings and marks and metadata + return vim.deepcopy(snipstr_or_str) + end + end, self), + SnippetString_mt + ) end -- copy without copying snippets. @@ -181,7 +192,7 @@ end -- where o is string, string[] or SnippetString. local function to_snippetstring(o) if type(o) == "string" then - return M.new({o}) + return M.new({ o }) elseif getmetatable(o) == SnippetString_mt then return o else @@ -247,7 +258,7 @@ local function find(self, start_i, i_inc, char_i, snipstr_map) v_str = v end - local current_str_to = current_str_from + #v_str-1 + local current_str_to = current_str_from + #v_str - 1 if char_i >= current_str_from and char_i <= current_str_to then return i end @@ -265,7 +276,7 @@ local function nodetext_len(node, snipstr_map) return #snipstr_map[node.static_text].str else -- +1 for each newline. - local len = #node.static_text-1 + local len = #node.static_text - 1 for _, v in ipairs(node.static_text) do len = len + #v end @@ -281,7 +292,7 @@ local function _replace(self, replacements, snipstr_map) for i = #replacements, 1, -1 do local repl = replacements[i] - local v_i_to = find(self, v_i_search_from, -1 , repl.to, snipstr_map) + local v_i_to = find(self, v_i_search_from, -1, repl.to, snipstr_map) local v_i_from = find(self, v_i_to, -1, repl.from, snipstr_map) -- next range may begin in v_i_from, before the currently inserted @@ -303,10 +314,15 @@ local function _replace(self, replacements, snipstr_map) pre = function(node) local node_len = nodetext_len(node, snipstr_map) if node_len > 0 then - local node_relative_repl_from = repl.from - node_from+1 - local node_relative_repl_to = repl.to - node_from+1 - - if node_relative_repl_from >= 1 and node_relative_repl_from <= node_len then + local node_relative_repl_from = repl.from + - node_from + + 1 + local node_relative_repl_to = repl.to - node_from + 1 + + if + node_relative_repl_from >= 1 + and node_relative_repl_from <= node_len + then if node_relative_repl_to <= node_len then if M.isinstance(node.static_text) then -- node contains a snippetString, recurse! @@ -314,7 +330,11 @@ local function _replace(self, replacements, snipstr_map) -- snipstr_map, we don't even have to -- modify repl to be defined based on the -- other snippetString. (ie. shift from and to) - _replace(node.static_text, {repl}, snipstr_map) + _replace( + node.static_text, + { repl }, + snipstr_map + ) else -- simply manipulate the node-static-text -- manually. @@ -325,12 +345,24 @@ local function _replace(self, replacements, snipstr_map) -- the only data in snipstr_map we may -- access that is inaccurate), the queries -- will still be answered correctly. - local str = table.concat(node.static_text, "\n") + local str = + table.concat(node.static_text, "\n") node.static_text = vim.split( - str:sub(1, node_relative_repl_from-1) .. repl.str .. str:sub(node_relative_repl_to+1), "\n") + str:sub(1, node_relative_repl_from - 1) + .. repl.str + .. str:sub( + node_relative_repl_to + 1 + ), + "\n" + ) end -- update string in snipstr_map. - snipstr_map[snip] = snipstr_map[snip]:sub(1, repl.from - v_from_from-1) .. repl.str .. snipstr_map[snip]:sub(repl.to - v_to_from+1) + snipstr_map[snip] = snipstr_map[snip]:sub( + 1, + repl.from - v_from_from - 1 + ) .. repl.str .. snipstr_map[snip]:sub( + repl.to - v_to_from + 1 + ) error(true) else -- range begins in, but ends outside this node @@ -343,16 +375,21 @@ local function _replace(self, replacements, snipstr_map) node_from = node_from + node_len end end, - post = util.nop + post = util.nop, }) end -- in lieu of `continue`, we need this bool to check whether we did a replacement yet. if not repl_in_node then - local from_str = self[v_i_from].snip and snipstr_map[self[v_i_from].snip] or self[v_i_from] - local to_str = self[v_i_to].snip and snipstr_map[self[v_i_to].snip] or self[v_i_to] + local from_str = self[v_i_from].snip + and snipstr_map[self[v_i_from].snip] + or self[v_i_from] + local to_str = self[v_i_to].snip and snipstr_map[self[v_i_to].snip] + or self[v_i_to] -- +1 to get the char of to, +1 to start beyond it. - self[v_i_from] = from_str:sub(1, repl.from - v_from_from) .. repl.str .. to_str:sub(repl.to - v_to_from+1+1) + self[v_i_from] = from_str:sub(1, repl.from - v_from_from) + .. repl.str + .. to_str:sub(repl.to - v_to_from + 1 + 1) -- start-position of string has to be updated. snipstr_map[self][v_i_from] = v_from_from end @@ -361,11 +398,11 @@ local function _replace(self, replacements, snipstr_map) -- take note that repl_from and repl_to are given wrt. the outermost -- snippet_string, and mark.pos is relative to self. -- So, these have to be converted to and from. - local self_offset = snipstr_map[self][1]-1 + local self_offset = snipstr_map[self][1] - 1 for _, mark in ipairs(self.marks) do if repl.to < mark.pos + self_offset then -- mark shifted to the right. - mark.pos = mark.pos - (repl.to - repl.from+1) + #repl.str + mark.pos = mark.pos - (repl.to - repl.from + 1) + #repl.str elseif repl.from < mark.pos + self_offset then -- we already know that repl.to >= mark.pos. -- This means that the marker is inside the deleted region, and @@ -373,7 +410,8 @@ local function _replace(self, replacements, snipstr_map) -- For now, shift the mark to the beginning or end of the newly -- inserted text, depending on rgrav. - mark.pos = (mark.rgrav and repl.to+1 or repl.from) - self_offset + mark.pos = (mark.rgrav and repl.to + 1 or repl.from) + - self_offset end -- in this case the replacement is completely behind the marks -- position, don't have to change it. @@ -401,7 +439,7 @@ local function upper(self) end end end, - post = util.nop + post = util.nop, }) else self[i] = v:upper() @@ -422,7 +460,7 @@ local function lower(self) end end end, - post = util.nop + post = util.nop, }) else self[i] = v:lower() @@ -466,7 +504,7 @@ function SnippetString:gsub(pattern, repl) table.insert(replacements, { from = match_from, to = match_to, - str = str:sub(match_from, match_to):gsub(pattern, repl) + str = str:sub(match_from, match_to):gsub(pattern, repl), }) end find_from = match_to + 1 @@ -494,7 +532,7 @@ function SnippetString:sub(from, to) -- empty range => return empty snippetString. if from > #str or to < from or to < 1 then - return M.new({""}) + return M.new({ "" }) end from = math.max(from, 1) @@ -503,18 +541,17 @@ function SnippetString:sub(from, to) local replacements = {} -- from <= 1 => don't need to remove from beginning. if from > 1 then - table.insert(replacements, { from=1, to=from-1, str = "" }) + table.insert(replacements, { from = 1, to = from - 1, str = "" }) end -- to >= #str => don't need to remove from end. if to < #str then - table.insert(replacements, { from=to+1, to=#str, str = "" }) + table.insert(replacements, { from = to + 1, to = #str, str = "" }) end _replace(self, replacements, snipstr_map) return self end - -- add a kind-of extmark to the text in this buffer. It moves with inserted -- text, and has a gravity to control into which direction it shifts. -- pos is 1-based and refers to one character in the string, rgrav = true can be @@ -537,7 +574,8 @@ function SnippetString:add_mark(id, pos, rgrav) table.insert(self.marks, { id = id, pos = pos + (rgrav and 1 or 0), - rgrav = rgrav}) + rgrav = rgrav, + }) end function SnippetString:get_mark_pos(id) diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 810e02b94..cfaf9dc56 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -75,7 +75,8 @@ end local function cursor_set_keys(pos, before) if before then if pos[2] == 0 then - local prev_line_str = vim.api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1] + local prev_line_str = + vim.api.nvim_buf_get_lines(0, pos[1] - 1, pos[1], false)[1] if prev_line_str then -- set onto last column of previous line, if possible. pos[1] = pos[1] - 1 @@ -116,7 +117,8 @@ end function M.select_range(b, e) local id = next_id() - enqueued_cursor_state = {pos = vim.deepcopy(b), pos_v = vim.deepcopy(e), mode = "s", id = id} + enqueued_cursor_state = + { pos = vim.deepcopy(b), pos_v = vim.deepcopy(e), mode = "s", id = id } enqueue_action(function() -- stylua: ignore _feedkeys_insert(id, @@ -149,7 +151,7 @@ end -- move the cursor to a position and enter insert-mode (or stay in it). function M.insert_at(pos) local id = next_id() - enqueued_cursor_state = {pos = pos, mode = "i", id = id} + enqueued_cursor_state = { pos = pos, mode = "i", id = id } enqueue_action(function() -- if current and target mode is INSERT, there's no reason to leave it. @@ -172,10 +174,10 @@ end function M.move_to_normal(pos) local id = next_id() -- preserve mode. - enqueued_cursor_state = {pos = pos, mode = "n", id = id} + enqueued_cursor_state = { pos = pos, mode = "n", id = id } enqueue_action(function() - if vim.fn.mode():sub(1,1) == "n" then + if vim.fn.mode():sub(1, 1) == "n" then util.set_cursor_0ind(pos) M.confirm(id) else @@ -211,16 +213,16 @@ function M.last_state() local state = {} local getposdot = vim.fn.getpos(".") - state.pos = {getposdot[2]-1, getposdot[3]-1} + state.pos = { getposdot[2] - 1, getposdot[3] - 1 } local getposv = vim.fn.getpos("v") -- store selection-range with end-position one column after the cursor -- at the end (so -1 to make getpos-position 0-based, +1 to move it one -- beyond the last character of the range) - state.pos_v = {getposv[2]-1, getposv[3]} + state.pos_v = { getposv[2] - 1, getposv[3] } -- only store first component. - state.mode = vim.fn.mode():sub(1,1) + state.mode = vim.fn.mode():sub(1, 1) return state end diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 80dd2ef20..834e9953a 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -122,7 +122,7 @@ function M.multiline_substr(str, from, to) -- include all rows for i = from[1], to[1] do - table.insert(res, str[i+1]) + table.insert(res, str[i + 1]) end -- trim text before from and after to. @@ -130,7 +130,7 @@ function M.multiline_substr(str, from, to) -- on the same line. If res[1] was trimmed first, we'd have to adjust the -- trim-point of `to`. res[#res] = res[#res]:sub(1, to[2]) - res[1] = res[1]:sub(from[2]+1) + res[1] = res[1]:sub(from[2] + 1) return res end @@ -158,7 +158,7 @@ end -- given in utf-codepoints and 0-based) into an offset (in bytes!, 1-based) for -- the \n-concatenated version of that string. function M.multiline_to_byte_offset(str, pos) - if pos[1] < 0 or pos[1]+1 > #str or pos[2] < 0 then + if pos[1] < 0 or pos[1] + 1 > #str or pos[2] < 0 then -- pos is trivially (row negative or beyond str, or col negative) -- outside of str, can't represent position in str. -- col-wise outside will be determined later, but we want this @@ -169,12 +169,12 @@ function M.multiline_to_byte_offset(str, pos) local byte_pos = 0 for i = 1, pos[1] do -- increase index by full lines, don't forget +1 for \n. - byte_pos = byte_pos + #str[i]+1 + byte_pos = byte_pos + #str[i] + 1 end -- allow positions one beyond the last character for all lines (even the -- last line). - local pos_line_str = str[pos[1]+1] .. "\n" + local pos_line_str = str[pos[1] + 1] .. "\n" if pos[2] >= #pos_line_str then -- in this case, pos is outside of the multiline-region. @@ -183,7 +183,7 @@ function M.multiline_to_byte_offset(str, pos) byte_pos = byte_pos + vim.str_byteindex(pos_line_str, pos[2]) -- 0- to 1-based columns - return byte_pos+1 + return byte_pos + 1 end -- inverse of multiline_to_byte_offset, 1-based byte to 0,0-based row,column, utf-aware. @@ -194,11 +194,12 @@ function M.byte_to_multiline_offset(str, byte_pos) local byte_pos_so_far = 0 for i, line in ipairs(str) do - local line_i_end = byte_pos_so_far + #line+1 + local line_i_end = byte_pos_so_far + #line + 1 if byte_pos <= line_i_end then -- byte located in this line, find utf-index. - local utf16_index = vim.str_utfindex(line .. "\n", byte_pos - byte_pos_so_far-1) - return {i-1, utf16_index} + local utf16_index = + vim.str_utfindex(line .. "\n", byte_pos - byte_pos_so_far - 1) + return { i - 1, utf16_index } end byte_pos_so_far = line_i_end end diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 90010efa9..cfd2db910 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -394,7 +394,7 @@ end -- Assumption: `pos` occurs after `base_pos`. local function pos_offset(base_pos, pos) local row_offset = pos[1] - base_pos[1] - return {row_offset, row_offset == 0 and pos[2] - base_pos[2] or pos[2]} + return { row_offset, row_offset == 0 and pos[2] - base_pos[2] or pos[2] } end -- compute offset of `pos` into multiline string starting at `base_pos`. @@ -402,7 +402,10 @@ end -- when `pos` is on a line different from `base_pos`. -- Assumption: `pos` occurs after `base_pos`. local function pos_from_offset(base_pos, offset) - return {base_pos[1]+offset[1], offset[1] == 0 and base_pos[2] + offset[2] or offset[2]} + return { + base_pos[1] + offset[1], + offset[1] == 0 and base_pos[2] + offset[2] or offset[2], + } end local function shallow_copy(t) @@ -458,5 +461,5 @@ return { pos_cmp = pos_cmp, pos_offset = pos_offset, pos_from_offset = pos_from_offset, - shallow_copy = shallow_copy + shallow_copy = shallow_copy, } diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index f0b6bb755..e1b8525b0 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -311,7 +311,9 @@ describe("ChoiceNode", function() end) it("correctly gives current content of choices.", function() - assert.are.same({"${1:asdf}", "qwer"}, exec_lua[[ + assert.are.same( + { "${1:asdf}", "qwer" }, + exec_lua([[ ls.snip_expand(s("trig", { c(1, { i(1, "asdf"), @@ -321,10 +323,13 @@ describe("ChoiceNode", function() ls.change_choice() return ls.get_current_choices() ]]) + ) end) it("correctly restores the generated node of a dynamicNode.", function() - assert.are.same({ "${1:${${1:aaa}${2:${1:aaa}}}}$0" }, exec_lua[[ + assert.are.same( + { "${1:${${1:aaa}${2:${1:aaa}}}}$0" }, + exec_lua([[ snip = s("trig", { c(1, { r(nil, "restore_key", { @@ -339,24 +344,25 @@ describe("ChoiceNode", function() }) return snip:get_docstring() ]]) + ) exec_lua("ls.snip_expand(snip)") feed("qwer") exec_lua("ls.jump(1)") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ qwer^q{3:wer} | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua("ls.change_choice(1)") -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ a^q{3:wer}qwera | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) end) it("cursor is correctly restored after change", function() @@ -374,7 +380,7 @@ screen:expect({ [3] = { background = Screen.colors.LightGray }, }) - exec_lua[=[ + exec_lua([=[ ls.snip_expand(s("trig", { c(1, { fmt([[ @@ -389,13 +395,13 @@ screen:expect({ ]], {r(1, "name", i(1, "fname")), r(2, "body", i(1, "fbody"))}) }, {restore_cursor = true}) })) - ]=] + ]=]) exec_lua("vim.wait(10, function() end)") - exec_lua"ls.jump(1)" + exec_lua("ls.jump(1)") feed("asdfasdfqweraaaa") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ local fname = function() | aaaa | bbbbasdf | @@ -403,11 +409,11 @@ screen:expect({ qwer | aa^aa | {2:-- INSERT --} | - ]] -}) - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ local function fname() | asdf | asdf | @@ -415,12 +421,12 @@ screen:expect({ aa^aa | end | {2:-- INSERT --} | - ]] -}) - exec_lua"ls.jump(-1)" - exec_lua"ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(-1)") + exec_lua("ls.jump(1)") + screen:expect({ + grid = [[ local function fname() | ^a{3:sdf} | {3:asdf} | @@ -428,11 +434,11 @@ screen:expect({ {3: aaaa} | end | {2:-- SELECT --} | - ]] -}) - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ aaaa | bbbb^a{3:sdf} | {3:asdf} | @@ -440,11 +446,11 @@ screen:expect({ {3: aaaa} | end | {2:-- SELECT --} | - ]] -}) + ]], + }) feed("i") - exec_lua"ls.change_choice(1)" - exec_lua[=[ + exec_lua("ls.change_choice(1)") + exec_lua([=[ ls.snip_expand(s("for", { t"for ", c(1, { sn(nil, {i(1, "k"), t", ", i(2, "v"), t" in ", c(3, {{t"pairs(",i(1),t")"}, {t"ipairs(",i(1),t")"}, i(nil)}, {restore_cursor = true}) }), @@ -453,9 +459,9 @@ screen:expect({ fmt([[{} in vim.gsplit({})]], {i(1, "str"), i(2)}) }, {restore_cursor = true}), t{" do", "\t"}, isn(2, {dl(1, l.LS_SELECT_DEDENT)}, "$PARENT_INDENT\t"), t{"", "end"} })) - ]=] -screen:expect({ - grid = [[ + ]=]) + screen:expect({ + grid = [[ local function fname() | for ^k, v in pairs() do | | @@ -463,11 +469,11 @@ screen:expect({ end | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ local function fname() | for ^v{3:al} in do | | @@ -475,12 +481,12 @@ screen:expect({ end | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1)" - exec_lua"ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1)") + exec_lua("ls.jump(1)") + screen:expect({ + grid = [[ local function fname() | for val in do | ^ | @@ -488,11 +494,11 @@ screen:expect({ end | {0:~ }| {2:-- INSERT --} | - ]] -}) - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ local fname = function() | aaaa | bbbbfor val in do | @@ -500,12 +506,12 @@ screen:expect({ endi | end | {2:-- INSERT --} | - ]] -}) + ]], + }) end) it("select_choice works.", function() - exec_lua[=[ + exec_lua([=[ ls.snip_expand(s("for", { t"for ", c(1, { sn(nil, {i(1, "k"), t", ", i(2, "v"), t" in ", c(3, {{t"pairs(",i(1),t")"}, {t"ipairs(",i(1),t")"}, i(nil)}, {restore_cursor = true}) }), @@ -514,20 +520,20 @@ screen:expect({ fmt([[{} in vim.gsplit({})]], {i(1, "str"), i(2)}) }, {restore_cursor = true}), t{" do", "\t"}, isn(2, {dl(1, l.LS_SELECT_DEDENT)}, "$PARENT_INDENT\t"), t{"", "end"} })) - ]=] + ]=]) feed("lua require('luasnip.extras.select_choice')()2") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ for ^v{3:al} in do | | {2:-- SELECT --} | - ]] -}) + ]], + }) feed("aa") -- simulate vim.ui.select that modifies the cursor. -- Can happen in the wild with plugins like dressing.nvim (although -- those usually just leave INSERT), and we would like to prevent it. - exec_lua[[ + exec_lua([[ vim.ui.select = function(_,_,cb) vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes( @@ -541,20 +547,20 @@ screen:expect({ cb(nil, 2) end - ]] + ]]) -- re-selecting correctly highlights text again (test by editing so the test does not pass immediately, without any changes!) exec_lua("require('luasnip.extras.select_choice')()") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ for a^a in do | | {2:-- INSERT --} | - ]] -}) + ]], + }) end) it("updates the active node before changing choice.", function() - exec_lua[[ + exec_lua([[ ls.setup({ link_children = true }) @@ -572,35 +578,35 @@ screen:expect({ }, {restore_cursor = true}), t":" })) - ]] - exec_lua"ls.jump(1)" + ]]) + exec_lua("ls.jump(1)") feed("i aa ") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ :ccee a^a ee: | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) -- if we wouldn't update before the change_choice, the last_args of the -- restored dynamicNode would not fit its current content, and we'd -- lose the text inserted until now due to the update (as opposed to -- a proper restore of dynamicNode.snip, which should occur in a -- restoreNode). - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ :.ccee ee^ee ee.: | {0:~ }| {2:-- INSERT --} | - ]] -}) - exec_lua"ls.set_choice(2)" -screen:expect({ unchanged = true }) + ]], + }) + exec_lua("ls.set_choice(2)") + screen:expect({ unchanged = true }) -- test some more wild stuff, just because. feed(" ") - exec_lua[[ + exec_lua([[ ls.snip_expand(s("trig", { t":", c(1, { @@ -615,54 +621,54 @@ screen:expect({ unchanged = true }) }, {restore_cursor = true}), t":" })) - ]] + ]]) -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ :.ccee e :^c{3:c}eeee:eee ee.: | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1)" + ]], + }) + exec_lua("ls.jump(1)") feed("i aa ") - exec_lua"ls.set_choice(2)" -screen:expect({ - grid = [[ + exec_lua("ls.set_choice(2)") + screen:expect({ + grid = [[ :.ccee e :.ccee ee^ee ee.:eee ee.: | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) -- reselect outer choiceNode - exec_lua"ls.jump(-1)" - exec_lua"ls.jump(-1)" - exec_lua"ls.jump(-1)" - exec_lua"ls.jump(1)" -screen:expect({ - grid = [[ + exec_lua("ls.jump(-1)") + exec_lua("ls.jump(-1)") + exec_lua("ls.jump(-1)") + exec_lua("ls.jump(1)") + screen:expect({ + grid = [[ :.cc^e{3:e e :.ccee eeee ee.:eee ee}.: | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.change_choice(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.change_choice(1)") + screen:expect({ + grid = [[ :cc^e{3:e e :.ccee eeee ee.:eee ee}: | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1)" - exec_lua"ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1)") + exec_lua("ls.jump(1)") + screen:expect({ + grid = [[ :ccee e :.cc^e{3:e eeee ee}.:eee ee: | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) end) end) diff --git a/tests/integration/dynamic_spec.lua b/tests/integration/dynamic_spec.lua index 900385eae..c70ea8e37 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -383,26 +383,29 @@ describe("DynamicNode", function() end, {opt(k("ins"))}) })) ]]) -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ ^e{3:sdf} | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) feed("aaaaa") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ eeeee^ | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) end) - it("selected text is selected again after updating (when possible).", function() - - assert.are.same({"${1:${1:esdf}}$0"}, exec_lua[[ + it( + "selected text is selected again after updating (when possible).", + function() + assert.are.same( + { "${1:${1:esdf}}$0" }, + exec_lua([[ snip = s("trig", { d(1, function(args) if not args[1] then @@ -414,22 +417,24 @@ screen:expect({ }) return snip:get_docstring() ]]) - exec_lua[[ + ) + exec_lua([[ ls.snip_expand(snip) - ]] - feed("a") - exec_lua("ls.lsp_expand('${1:asdf}')") -screen:expect({ - grid = [[ + ]]) + feed("a") + exec_lua("ls.lsp_expand('${1:asdf}')") + screen:expect({ + grid = [[ e^e{3:sdf}sdf | {0:~ }| {2:-- SELECT --} | - ]] -}) - end) + ]], + }) + end + ) it("cursor-position is moved with text-manipulations.", function() - exec_lua[[ + exec_lua([[ ls.snip_expand(s("trig", { d(1, function(args) if not args[1] then @@ -439,23 +444,23 @@ screen:expect({ end end, {opt(k("ins"))}, {snippetstring_args = true}) })) - ]] + ]]) -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ ^e{3:esdf} | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) feed("aaaaaa") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ eeeeee^eeeeee | {0:~ }| | - ]] -}) + ]], + }) end) it("") diff --git a/tests/integration/restore_spec.lua b/tests/integration/restore_spec.lua index ac2f94007..4d441ead8 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -367,25 +367,24 @@ describe("RestoreNode", function() feed(". .") exec_lua("ls.lsp_expand('($1)')") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ a: . (^) . | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) exec_lua("ls.change_choice(1)") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ b: . (^) . | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) end) it("correctly restores snippets (2).", function() - exec_lua([[ ls.setup({link_children = true}) ls.snip_expand(s("trig", { @@ -397,32 +396,31 @@ screen:expect({ end, {1}) })) ]]) - exec_lua[[ls.jump(1)]] + exec_lua([[ls.jump(1)]]) feed(". .") exec_lua("ls.lsp_expand('($1)')") feed("i") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ asdf . (i^) .asdf | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) exec_lua("ls.jump(-1) ls.jump(-1)") feed("qwer") exec_lua("ls.jump(1) ls.jump(1)") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ qwer . (^i) .qwer | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) end) -- make sure store and update_restore propagate. it("correctly restores snippets (3).", function() - exec_lua([[ ls.setup({link_children = true}) ls.snip_expand(s("trig", { @@ -434,7 +432,7 @@ screen:expect({ end, {1}) })) ]]) - exec_lua[[ls.jump(1)]] + exec_lua([[ls.jump(1)]]) feed(". .") exec_lua([[ ls.snip_expand(s("trig", { @@ -442,28 +440,27 @@ screen:expect({ })) ]]) feed("i") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ asdf . (i^) .asdf | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) exec_lua("ls.jump(-1) ls.jump(-1)") feed("qwer") exec_lua("ls.jump(1) ls.jump(1)") -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ qwer . (^i) .qwer | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) end) -- make sure store and update_restore propagate. it("correctly restores snippets (4).", function() - exec_lua([[ ls.setup({link_children = true}) ls.snip_expand(s("trig", { @@ -475,7 +472,7 @@ screen:expect({ end, {1}) })) ]]) - exec_lua[[ls.jump(1)]] + exec_lua([[ls.jump(1)]]) local function exp() exec_lua([[ @@ -487,66 +484,66 @@ screen:expect({ end exp() - exec_lua"ls.jump(1)" + exec_lua("ls.jump(1)") exp() - exec_lua"ls.jump(1)" + exec_lua("ls.jump(1)") exp() feed("i") exp() exp() exp() -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ asdf (i)(i)(i (i(i(i^))) i)asdf | {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) -- 11x to get back to the i1. - exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" - exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" - exec_lua"ls.jump(-1) ls.jump(-1) ls.jump(-1)" - exec_lua"ls.jump(-1) ls.jump(-1)" + exec_lua("ls.jump(-1) ls.jump(-1) ls.jump(-1)") + exec_lua("ls.jump(-1) ls.jump(-1) ls.jump(-1)") + exec_lua("ls.jump(-1) ls.jump(-1) ls.jump(-1)") + exec_lua("ls.jump(-1) ls.jump(-1)") feed("qwer") - exec_lua"ls.jump(1)" -screen:expect({ - grid = [[ + exec_lua("ls.jump(1)") + screen:expect({ + grid = [[ qwer ^({3:i)(i)(i (i(i(i))) i)}qwer | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1) ls.jump(1) ls.jump(1)") + screen:expect({ + grid = [[ qwer (i)(^i)(i (i(i(i))) i)qwer | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1) ls.jump(1) ls.jump(1)") + screen:expect({ + grid = [[ qwer (i)(i)(i (^i{3:(i(i))}) i)qwer | {0:~ }| {2:-- SELECT --} | - ]] -}) - exec_lua"ls.jump(1) ls.jump(1) ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1) ls.jump(1) ls.jump(1)") + screen:expect({ + grid = [[ qwer (i)(i)(i (i(i(i)^)) i)qwer | {0:~ }| {2:-- INSERT --} | - ]] -}) - exec_lua"ls.jump(1) ls.jump(1) ls.jump(1) ls.jump(1)" -screen:expect({ - grid = [[ + ]], + }) + exec_lua("ls.jump(1) ls.jump(1) ls.jump(1) ls.jump(1)") + screen:expect({ + grid = [[ qwer (i)(i)(i (i(i(i))) i)^q{3:wer} | {0:~ }| {2:-- SELECT --} | - ]] -}) + ]], + }) end) end) diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index f99fc684b..dac58df96 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -386,11 +386,11 @@ describe("session", function() -- this fail only here specifically (IIRC there are enough tests that -- do something similar)), and since it's fine on 0.9 and master (which -- matter much more) there shouldn't be an issue in practice. - exec_lua[[ + exec_lua([[ if require("luasnip.util.vimversion").ge(0,8,0) then ls.jump(1) end - ]] + ]]) end) it("Deleting nested snippet only removes it.", function() feed("ofn") @@ -2294,8 +2294,8 @@ describe("session", function() change(1) change(1) -- currently wrong! -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -2326,7 +2326,7 @@ screen:expect({ {0:~ }| {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) end) end) diff --git a/tests/unit/str_spec.lua b/tests/unit/str_spec.lua index 53a397061..7471c8c19 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -68,18 +68,50 @@ describe("str.multiline_substr", function() local function check(dscr, str, from, to, expected) it(dscr, function() - assert.are.same(expected, exec_lua([[ + assert.are.same( + expected, + exec_lua( + [[ local str, from, to = ... return require("luasnip.util.str").multiline_substr(str, from, to) - ]], str, from, to)) + ]], + str, + from, + to + ) + ) end) end - check("entire range", {"asdf", "qwer"}, {0,0}, {1,4}, {"asdf", "qwer"}) - check("partial range", {"asdf", "qwer"}, {0,3}, {1,2}, {"f", "qw"}) - check("another partial range", {"asdf", "qwer"}, {1,2}, {1,3}, {"e"}) - check("one last partial range", {"asdf", "qwer", "zxcv"}, {0,2}, {2,4}, {"df", "qwer", "zxcv"}) - check("empty range", {"asdf", "qwer", "zxcv"}, {0,2}, {0,2}, {""}) + check( + "entire range", + { "asdf", "qwer" }, + { 0, 0 }, + { 1, 4 }, + { "asdf", "qwer" } + ) + check( + "partial range", + { "asdf", "qwer" }, + { 0, 3 }, + { 1, 2 }, + { "f", "qw" } + ) + check( + "another partial range", + { "asdf", "qwer" }, + { 1, 2 }, + { 1, 3 }, + { "e" } + ) + check( + "one last partial range", + { "asdf", "qwer", "zxcv" }, + { 0, 2 }, + { 2, 4 }, + { "df", "qwer", "zxcv" } + ) + check("empty range", { "asdf", "qwer", "zxcv" }, { 0, 2 }, { 0, 2 }, { "" }) end) describe("str.multiline_to_byte_offset", function() @@ -89,33 +121,48 @@ describe("str.multiline_to_byte_offset", function() local function check(dscr, str, multiline_pos, byte_pos) it(dscr, function() - assert.are.same(byte_pos, exec_lua([[ + assert.are.same( + byte_pos, + exec_lua( + [[ local str, multiline_pos = ... return require("luasnip.util.str").multiline_to_byte_offset(str, multiline_pos) - ]], str, multiline_pos)) + ]], + str, + multiline_pos + ) + ) end) end local function check_is_nil(dscr, str, multiline_pos, byte_pos) it(dscr, function() - assert(exec_lua([[ + assert(exec_lua( + [[ local str, multiline_pos = ... return require("luasnip.util.str").multiline_to_byte_offset(str, multiline_pos) == nil - ]], str, multiline_pos)) + ]], + str, + multiline_pos + )) end) end - check("single line begin", {"asdf"}, {0,0}, 1) - check("single line middle", {"asdf"}, {0,2}, 3) - check("single line end", {"asdf"}, {0,3}, 4) - check("single line, on \n", {"asdf"}, {0,4}, 5) - check_is_nil("single line, outside of range", {"asdf"}, {0,5}) - check("multiple lines", {"asdf", "qwer"}, {1,0}, 6) - check("multiple lines middle", {"asdf", "qwer"}, {1,3}, 9) - check_is_nil("multiple lines outside of range row", {"asdf", "qwer"}, {2,0}) - check("on linebreak", {"asdf", "qwer"}, {0,4}, 5) - check("on linebreak of last line", {"asdf", "qwer"}, {1,4}, 10) - check_is_nil("negative row", {"asdf", "qwer"}, {-1,0}) - check_is_nil("negative col", {"asdf", "qwer"}, {0,-2}) + check("single line begin", { "asdf" }, { 0, 0 }, 1) + check("single line middle", { "asdf" }, { 0, 2 }, 3) + check("single line end", { "asdf" }, { 0, 3 }, 4) + check("single line, on \n", { "asdf" }, { 0, 4 }, 5) + check_is_nil("single line, outside of range", { "asdf" }, { 0, 5 }) + check("multiple lines", { "asdf", "qwer" }, { 1, 0 }, 6) + check("multiple lines middle", { "asdf", "qwer" }, { 1, 3 }, 9) + check_is_nil( + "multiple lines outside of range row", + { "asdf", "qwer" }, + { 2, 0 } + ) + check("on linebreak", { "asdf", "qwer" }, { 0, 4 }, 5) + check("on linebreak of last line", { "asdf", "qwer" }, { 1, 4 }, 10) + check_is_nil("negative row", { "asdf", "qwer" }, { -1, 0 }) + check_is_nil("negative col", { "asdf", "qwer" }, { 0, -2 }) end) describe("byte_to_multiline_offset", function() @@ -125,28 +172,39 @@ describe("byte_to_multiline_offset", function() local function check(dscr, str, byte_pos, multiline_pos) it(dscr, function() - assert.are.same(multiline_pos, exec_lua([[ + assert.are.same( + multiline_pos, + exec_lua( + [[ local str, byte_pos = ... return require("luasnip.util.str").byte_to_multiline_offset(str, byte_pos) - ]], str, byte_pos)) + ]], + str, + byte_pos + ) + ) end) end local function check_is_nil(dscr, str, byte_pos, multiline_pos) it(dscr, function() - assert(exec_lua([[ + assert(exec_lua( + [[ local str, byte_pos = ... return require("luasnip.util.str").byte_to_multiline_offset(str, byte_pos) == nil - ]], str, byte_pos)) + ]], + str, + byte_pos + )) end) end - check("single line begin", {"asdf"}, 1, {0,0}) - check("single line middle", {"asdf"}, 3, {0,2}) - check("single line end", {"asdf"}, 4, {0,3}) - check("single line on linebreak", {"asdf"}, 5, {0,4}) - check("multiple lines", {"asdf", "qwer"}, 6, {1,0}) - check("multiple lines middle", {"asdf", "qwer"}, 9, {1,3}) - check("multiple lines middle linebreak", {"asdf", "qwer"}, 10, {1,4}) - check_is_nil("before string", {"asdf", "qwer"}, -1) - check_is_nil("multiple lines behind string", {"asdf", "qwer"}, 11) + check("single line begin", { "asdf" }, 1, { 0, 0 }) + check("single line middle", { "asdf" }, 3, { 0, 2 }) + check("single line end", { "asdf" }, 4, { 0, 3 }) + check("single line on linebreak", { "asdf" }, 5, { 0, 4 }) + check("multiple lines", { "asdf", "qwer" }, 6, { 1, 0 }) + check("multiple lines middle", { "asdf", "qwer" }, 9, { 1, 3 }) + check("multiple lines middle linebreak", { "asdf", "qwer" }, 10, { 1, 4 }) + check_is_nil("before string", { "asdf", "qwer" }, -1) + check_is_nil("multiple lines behind string", { "asdf", "qwer" }, 11) end)