From 7255290f31326ad41c5f694fe8e6b9b27a22cc07 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 28 Dec 2023 16:33:55 +0100 Subject: [PATCH 01/77] 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 64f1279a1..2ca4e0aac 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") ---@class LuaSnip.Node local Node = {} @@ -324,7 +325,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({ @@ -349,18 +355,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 7ca8fab1f72c1463878ccb198f568e3b076a53e9 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:26:01 +0100 Subject: [PATCH 02/77] 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 58dac2b2a..3c99533af 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -470,6 +470,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 = ChoiceNode.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 2ca4e0aac..d588cad9d 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -669,6 +669,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 9d9fad278..66ea5b23f 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1587,6 +1587,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 6787860d5b663d6cf3dcc600e90221b9e8c7af6e Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:30:34 +0100 Subject: [PATCH 03/77] 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 7280e30d540cd6572e54dd96f210c1d7a2a076a1 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 09:31:35 +0100 Subject: [PATCH 04/77] 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 93b78bd2b0305a6797cd5629851cdf0b345de929 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 5 May 2025 14:27:05 +0200 Subject: [PATCH 05/77] 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 | 26 ++++++++++++++++++++++++++ lua/luasnip/nodes/node.lua | 4 ++++ lua/luasnip/nodes/restoreNode.lua | 9 +++++++++ lua/luasnip/nodes/snippet.lua | 17 +++++++++++++++++ 6 files changed, 73 insertions(+) diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 3c99533af..b244c0895 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -194,6 +194,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 @@ -204,6 +205,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()) @@ -476,6 +479,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 = ChoiceNode.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 c5575d292..2e5dbe8e0 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() @@ -248,10 +255,12 @@ function InsertNode:exit() snip:remove_from_jumplist() 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 @@ -307,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 d588cad9d..f095851d1 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -675,6 +675,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 66ea5b23f..4b6ff4350 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1595,6 +1595,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 ef72739f9d121034e394636acf52b31cdd72584d Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Mar 2024 10:30:17 +0100 Subject: [PATCH 06/77] 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 | 195 ++++++++++++++++++++--------- 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, 298 insertions(+), 242 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 4dd94b5ce..b29d4c5c3 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -163,18 +163,59 @@ function API.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, @@ -182,7 +223,87 @@ 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 @@ -653,52 +774,12 @@ end --- Update all nodes that depend on the currently-active node. function API.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 b244c0895..51e632a86 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -29,18 +29,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 @@ -210,7 +198,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 @@ -309,7 +297,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 @@ -330,8 +318,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() @@ -432,20 +419,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 2e5dbe8e0..1473166f9 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 f095851d1..43d9fb866 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -203,78 +203,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 @@ -623,6 +551,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() @@ -669,12 +611,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 4b6ff4350..b313a5864 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -120,11 +120,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 @@ -375,16 +370,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 @@ -674,6 +659,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) @@ -783,7 +769,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 @@ -965,6 +951,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 @@ -1132,7 +1120,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) @@ -1332,23 +1319,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 @@ -1595,6 +1565,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() @@ -1612,6 +1589,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 81d21ea71..6875648a1 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2031,12 +2031,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 3be4f9694e8624d0304d7ace493efa6ff8b43614 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 10 Mar 2024 20:20:16 +0100 Subject: [PATCH 07/77] 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 b313a5864..0ed823a6f 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -777,6 +777,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 5cfa6440d3f1666db0166e42a8ece79b8e72aabd Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 24 Apr 2024 00:00:58 +0200 Subject: [PATCH 08/77] 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/snippet.lua | 4 ++++ lua/luasnip/nodes/util.lua | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 0ed823a6f..f7943650b 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -487,6 +487,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 5d175a83476c5ce87e8548aaecfc35affc7b3cb5 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 24 Apr 2024 00:03:00 +0200 Subject: [PATCH 09/77] 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 57be4397fb778ca44588706ad1e7d0b2673c60e8 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:03:49 +0200 Subject: [PATCH 10/77] 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 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index b29d4c5c3..0a4c82b66 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -582,6 +582,11 @@ function API.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(API.active_update_dependents) + return snip end From 28ee8de14b29eda9d7be6366446781fa8e688363 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:11:01 +0200 Subject: [PATCH 11/77] 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 0a4c82b66..d3f65ee93 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -184,7 +184,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 @@ -196,16 +197,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 @@ -213,7 +212,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() @@ -223,7 +223,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. @@ -257,11 +260,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) @@ -277,7 +286,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 @@ -291,7 +303,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 @@ -782,7 +797,10 @@ function API.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 diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 51e632a86..8a3ef662f 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -318,7 +318,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 43d9fb866..a92fea083 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") ---@class LuaSnip.Node local Node = {} @@ -175,6 +176,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 @@ -555,11 +563,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 f7943650b..ae8eb6f1b 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -663,7 +663,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) @@ -773,7 +772,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 047772a58..1b28e0726 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -157,6 +157,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. @@ -171,6 +201,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 a4445b729..3732587d5 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -422,6 +422,15 @@ local function default_tbl_get(default, t, ...) return default 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, @@ -465,4 +474,5 @@ return { validate = validate, str_utf32index = str_utf32index, default_tbl_get = default_tbl_get, + pos_offset = pos_offset } diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index 6875648a1..1fd6fecd4 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2035,7 +2035,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 f98c038c2..32bedd5f9 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -195,3 +195,24 @@ describe("str.convert_indent", function() assert.are.same(expected, result) end) 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 a32304d6e4671a7df6e3c1729464030dde476774 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 28 Oct 2024 14:33:50 +0100 Subject: [PATCH 12/77] 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 8a3ef662f..5ba41372f 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -258,12 +258,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 @@ -327,14 +327,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 a92fea083..9d08b71cf 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -215,8 +215,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 ae8eb6f1b..a5dabd81e 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -531,14 +531,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] @@ -571,13 +572,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 @@ -586,14 +587,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 @@ -612,23 +613,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) @@ -761,21 +762,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 } @@ -795,9 +781,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, @@ -1288,17 +1277,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 @@ -1594,6 +1586,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 c9e66998d..84f4d772f 100644 --- a/tests/integration/function_spec.lua +++ b/tests/integration/function_spec.lua @@ -179,8 +179,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 a02b350a80b8cb0223babbb349710f83ee51c26f Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 28 Oct 2024 21:04:54 +0100 Subject: [PATCH 13/77] 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 9d08b71cf..eb08be65f 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -176,11 +176,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() @@ -313,10 +319,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 445888ccd..132913240 100644 --- a/lua/luasnip/util/mark.lua +++ b/lua/luasnip/util/mark.lua @@ -198,8 +198,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 1539abc47aea09e5f8347e8fd576cc993bd95a78 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:09:40 +0100 Subject: [PATCH 14/77] 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 4917cdea76549570239f41e798f7d75323cce4df Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:20:50 +0100 Subject: [PATCH 15/77] 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 7978f185122acc348c1e1b9a068da876baf02bbf Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:22:57 +0100 Subject: [PATCH 16/77] 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 afcf2a79f83c3a68b086b17a5a14234db87e1b81 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:23:22 +0100 Subject: [PATCH 17/77] 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 0488636f4068ea7836ff00b828cc07d55a5e2c20 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 10:51:19 +0100 Subject: [PATCH 18/77] 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 0b68948de0a2967022f4f2ecd106a475f353b354 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 12:15:19 +0100 Subject: [PATCH 19/77] 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 5a8f31c12..5ef14940c 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -353,4 +353,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 6ac4c97033e9986b4c80ab514cb5ad8d83f51dd6 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:16:48 +0100 Subject: [PATCH 20/77] 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 5ba41372f..db0decfe8 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -286,8 +286,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 7e84074b3d99e9535c8da953d68ca6bc8e9ae560 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:17:17 +0100 Subject: [PATCH 21/77] 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 02dc83d67c1b264e5c2f736721d9e7f71879b386 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 16:17:47 +0100 Subject: [PATCH 22/77] 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 a5dabd81e..f30267c73 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1158,12 +1158,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 ef040dcf10d84dc919ce6d0426eb03d5e273728d Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 17:55:15 +0100 Subject: [PATCH 23/77] 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 5ef14940c..1ad0b62ab 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -457,6 +457,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 d4ddbb45382ad303871ba402c00ed32d81bc75b4 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 21:21:14 +0100 Subject: [PATCH 24/77] 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 eb08be65f..e2f1f1ca2 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -298,6 +298,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 924d7ddcc43dcd92901ddaf57beee378a659a4e7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 21:53:07 +0100 Subject: [PATCH 25/77] 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 9d2399b5f6980e416b4ad772264662f9700b7b53 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 29 Oct 2024 23:05:20 +0100 Subject: [PATCH 26/77] 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 e2f1f1ca2..d56d2c62f 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -265,7 +265,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) @@ -298,13 +298,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 @@ -326,10 +333,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 704192461..a1007f543 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -208,7 +208,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 1fd6fecd4..b12424509 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -379,8 +379,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 0ded7acad5ab02d81809c059167d439c967d6310 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:12:03 +0200 Subject: [PATCH 27/77] 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 3732587d5..1472094e2 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -431,6 +431,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, @@ -474,5 +485,6 @@ return { validate = validate, str_utf32index = str_utf32index, default_tbl_get = default_tbl_get, - pos_offset = pos_offset + pos_offset = pos_offset, + shallow_copy = shallow_copy } From b477c0722bc0435cc0681e06b91f7e648b467efe Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:18:44 +0100 Subject: [PATCH 28/77] 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 f30267c73..128322a31 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -544,6 +544,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 @@ -778,9 +782,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 e7d6627001c992c5ec09d3abbf059d22fdef6e42 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:20:40 +0100 Subject: [PATCH 29/77] 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 1b28e0726..d24b0c222 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -179,6 +179,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 49c72aee6fae50da5336fc166e0351a9dd87fb21 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 14:21:35 +0100 Subject: [PATCH 30/77] 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 11f5158f9..ba76dbe36 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -1416,6 +1416,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 3dc03f825059ddefde732abc620ef6868559ae86 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 30 Oct 2024 15:55:50 +0100 Subject: [PATCH 31/77] 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 d3f65ee93..f5042687e 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -199,7 +199,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 4091612e0a91587ba5dd6f9ee6391c9db974069b Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 13:27:09 +0100 Subject: [PATCH 32/77] 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 aedf56423abdb7328d90f81ac23c2a52edbe9766 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 13:34:24 +0100 Subject: [PATCH 33/77] 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 3b2414dc88f81ab337c4c5569cd073dc88477d49 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 18:29:57 +0100 Subject: [PATCH 34/77] 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 2537da05e787bbe8a6e53228d742cdd60badbb98 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 21:08:27 +0100 Subject: [PATCH 35/77] 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 4a0103e3923f6a3e1318cbbbd33fd7e6fd835713 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 22:14:51 +0100 Subject: [PATCH 36/77] 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 96f1e64e25ccb4474475ba28f106507ca33a7871 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sat, 2 Nov 2024 22:15:18 +0100 Subject: [PATCH 37/77] 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 68b2f392da495fc2ef4c24af550adc7481590a15 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 3 Nov 2024 10:22:13 +0100 Subject: [PATCH 38/77] 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 6198d3e4e..885064e95 100644 --- a/lua/luasnip/default_config.lua +++ b/lua/luasnip/default_config.lua @@ -53,6 +53,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 a21ef503c872a511766bfae0251aad28baba0822 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 3 Nov 2024 16:10:02 +0100 Subject: [PATCH 39/77] 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 f5042687e..1d7fc374c 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") @@ -187,6 +188,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 @@ -203,9 +210,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 ba074531c..85fabd457 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -366,4 +366,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 31ad740765bbe02835651e3e79aa663f73b3f137 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:17:49 +0100 Subject: [PATCH 40/77] 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 d56d2c62f..af13e4e2c 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -352,10 +352,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 8a6f1a938424690193529d2bf7fff5c272bc3f47 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:42:07 +0100 Subject: [PATCH 41/77] 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 1d7fc374c..556548486 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -185,13 +185,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 41e03ed99caf2c0988b23c6d6ce873d832eb9a51 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:14:24 +0200 Subject: [PATCH 42/77] 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 | 7 ++----- 2 files changed, 10 insertions(+), 6 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 556548486..73b1a4c06 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -610,10 +610,7 @@ function API.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(API.active_update_dependents) + API.active_update_dependents() return snip end @@ -809,7 +806,7 @@ function API.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 } From 4af87a1aa16b48603e7589a1c200c4db6a9ccd4f Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:17:41 +0200 Subject: [PATCH 43/77] optionally update a node differnt from the current node. --- lua/luasnip/init.lua | 60 ++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 73b1a4c06..0635dddcf 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -221,8 +221,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 = @@ -238,19 +238,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. @@ -260,7 +260,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 @@ -285,28 +285,43 @@ 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 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_current:focus() + session.current_nodes[vim.api.nvim_get_current_buf()] = upd_res.new_current + 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()] @@ -317,13 +332,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 @@ -803,17 +819,7 @@ end --- Update all nodes that depend on the currently-active node. function API.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 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 + update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()]) end --- Generate and store the docstrings for a list of snippets as generated by From 05e8dc0633921b75a6a04dcaafdd5c8f745fbd37 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:44:53 +0100 Subject: [PATCH 44/77] 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 af13e4e2c..738ed9d60 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -654,7 +654,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 bae30cb2b54267ce41e0988e870f90794355044d Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 19:46:00 +0100 Subject: [PATCH 45/77] 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 0635dddcf..d012fd200 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -779,6 +779,7 @@ function API.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 --- Set the currently active choice. @@ -798,6 +799,7 @@ function API.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 --- Get a string-representation of all the current choiceNode's choices. diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index db0decfe8..3de828c38 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -296,7 +296,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({ "" }) @@ -319,10 +319,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 f36059589381bf7d1c7ce2a923af6450d0bea112 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:23:58 +0200 Subject: [PATCH 46/77] move no_region_wrap back into main-module. --- lua/luasnip/init.lua | 24 +++++++++++++++++++---- lua/luasnip/util/util.lua | 10 ---------- tests/integration/snippet_basics_spec.lua | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index d012fd200..4eda84205 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -34,6 +34,18 @@ function API.get_active_snip() return node 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) + + local fn_res = fn(...) + return fn_res +end + + -- returns matching snippet (needs to be copied before usage!) and its expand- -- parameters(trigger and captures). params are returned here because there's -- no need to recalculate them. @@ -357,7 +369,7 @@ end function API.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 @@ -461,7 +473,7 @@ function API.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. @@ -770,7 +782,7 @@ function API.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, @@ -790,7 +802,7 @@ function API.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, @@ -1319,4 +1331,8 @@ API.log = require("luasnip.util.log") ---@class LuaSnip: LuaSnip.API, LuaSnip.LazyAPI ls = lazy_table(API, ls_lazy) + +-- internal stuff, e.g. for tests. +ls.no_region_check_wrap = no_region_check_wrap + return ls diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 1472094e2..bcc7ab66c 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -346,15 +346,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 @@ -473,7 +464,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 ba76dbe36..ef6a75474 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -64,7 +64,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 5e0bd6469d1d0f616bf1d0c80b002e2575545b7a Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:55:43 +0100 Subject: [PATCH 47/77] 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 be11293411ed3fb8f02b81fee8e8f7ab3e4a5d27 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:56:43 +0100 Subject: [PATCH 48/77] 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 f71ed755a0ebde38e476f14367ad834ff57d386f Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:57:29 +0100 Subject: [PATCH 49/77] 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 b12424509..2d6d296e0 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2243,6 +2243,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 301c600f8a3fe731270b66cb28931d421ad48202 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 20:57:51 +0100 Subject: [PATCH 50/77] 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 d7e5dc08c..ef1e08e64 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -209,6 +209,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 71163f061197f960128502381357c29b9f8203a0 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 4 Nov 2024 22:25:31 +0100 Subject: [PATCH 51/77] 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 4eda84205..77541ff44 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -36,12 +36,14 @@ 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() + 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 2d6d296e0..591260313 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2243,9 +2243,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 = [[ /** | @@ -2264,7 +2261,7 @@ describe("session", function() * | * @return | * | - * @throws | + * @throws cc | */ | private aa bb() {4:●} | throws cc { | From 5f307cc4c609798c3ec40a6e676e3d9a51d9dbb4 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 19:37:17 +0100 Subject: [PATCH 52/77] 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 3de828c38..0d63fa8ee 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -22,12 +22,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] @@ -113,6 +107,13 @@ end extend_decorator.register(ChoiceNode.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 @@ -216,7 +217,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 738ed9d60..af42d1109 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -379,8 +379,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 @@ -620,20 +620,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 128322a31..dc9b9235a 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1027,7 +1027,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 bdfdb8e4a92208684e370b8f78c729ecadcdf7ae Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 19:55:05 +0100 Subject: [PATCH 53/77] 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 1707e9335de34777742767db99d175118e664274 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 20:46:56 +0100 Subject: [PATCH 54/77] 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 8c91720429793111a27f4b9265914fa788a41de7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 21:04:10 +0100 Subject: [PATCH 55/77] 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 77541ff44..1c8b83b6c 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -229,9 +229,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 591260313..13740b0da 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -2277,4 +2277,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 3439a5b44e675124304f69218bb856a4bd5097eb Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 21:10:18 +0100 Subject: [PATCH 56/77] 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 af42d1109..b8933de1f 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -54,28 +54,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 @@ -182,9 +160,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 @@ -297,8 +272,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 a1007f543..efe8cd69a 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -308,4 +308,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 87637ea79142457637346383a2ddb7545ec6fdcc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 6 Nov 2024 23:28:31 +0100 Subject: [PATCH 57/77] 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 85fabd457..1fdf6aced 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -400,8 +400,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"})}) @@ -409,7 +410,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 b2dbd41f7dc3e2a618f4d35f479f44b932b4fa90 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 7 Nov 2024 12:52:28 +0100 Subject: [PATCH 58/77] 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 0d63fa8ee..99104b7a7 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -7,6 +7,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") ---@class LuaSnip.ChoiceNode.ItemNode: LuaSnip.Node @@ -281,8 +283,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() @@ -337,10 +339,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 bcc7ab66c..f63900d32 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -422,6 +422,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 = {} @@ -476,5 +484,6 @@ return { str_utf32index = str_utf32index, default_tbl_get = default_tbl_get, 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 efe8cd69a..acb8325f9 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -321,4 +321,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 cbca762acaff5d7a5ef14d5afbec4e544c0f7455 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:26:47 +0200 Subject: [PATCH 59/77] 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 1c8b83b6c..582815a80 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -178,43 +178,6 @@ function API.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) @@ -222,19 +185,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 = @@ -259,7 +211,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 } @@ -304,7 +256,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 } @@ -778,17 +730,24 @@ end --- Change the currently active choice. ---@param val 1|-1 Move one choice forward or backward. -function API.change_choice(val) +function API.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() @@ -796,7 +755,10 @@ end --- Set the currently active choice. ---@param choice_indx integer Index of the choice to switch to. -function API.set_choice(choice_indx) +function API.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") @@ -808,7 +770,8 @@ function API.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 99104b7a7..f552ac3e8 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -275,17 +275,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. @@ -337,17 +332,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 @@ -355,12 +341,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 acb8325f9..d9fd54d0a 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -500,6 +500,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 e088a0ef0d48431d453ea04a787e27d4f26eb9f7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 13 Nov 2024 11:58:28 +0100 Subject: [PATCH 60/77] 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 0fbe8325e5be8448cd16882f8aaf57507b4df2bb Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 22:56:26 +0200 Subject: [PATCH 61/77] 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 | 256 ++++++++++++++-------- 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, 422 insertions(+), 130 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 582815a80..d996d5fc3 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") @@ -34,15 +35,28 @@ function API.get_active_snip() return node end -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 @@ -178,19 +192,14 @@ function API.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() @@ -211,7 +220,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 } @@ -229,9 +238,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 @@ -249,14 +258,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 } @@ -281,11 +321,17 @@ 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() + 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) local node = session.current_nodes[vim.api.nvim_get_current_buf()] @@ -315,13 +361,10 @@ local function safe_jump_current(dir, no_move, dry_run) end end ---- Jump forwards or backwards ----@param dir 1|-1 Jump forward for 1, backward for -1. ----@return boolean _ `true` if a jump was performed, `false` otherwise. -function API.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 @@ -339,6 +382,13 @@ function API.jump(dir) end end +--- Jump forwards or backwards +---@param dir 1|-1 Jump forward for 1, backward for -1. +---@return boolean _ `true` if a jump was performed, `false` otherwise. +function API.jump(dir) + return api_do(_jump, dir) +end + --- Find the node the next jump will end up at. This will not work always, --- because we will not update the node before jumping, so if the jump would --- e.g. insert a new node between this node and its pre-update jump target, @@ -425,11 +475,9 @@ function API.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. - ---@class LuaSnip.Opts.SnipExpandExpandParams ---@field trigger? string What to set as the expanded snippets' trigger --- (Defaults to `snip.trigger`). @@ -509,12 +557,7 @@ end --- } --- ``` ---- Expand a snippet in the current buffer. ----@param snippet LuaSnip.Snippet The snippet. ----@param opts? LuaSnip.Opts.SnipExpand Optional additional arguments. ----@return LuaSnip.ExpandedSnippet _ The snippet that was inserted into the ---- buffer. -function API.snip_expand(snippet, opts) +local function _snip_expand(snippet, opts) local snip = snippet:copy() opts = opts or {} @@ -590,16 +633,26 @@ function API.snip_expand(snippet, opts) -- -1 to disable count. vim.cmd([[silent! call repeat#set("\luasnip-expand-repeat", -1)]]) - API.active_update_dependents() + _active_update_dependents() return snip end ---- Find a snippet whose trigger matches the text before the cursor and expand ---- it. ----@param opts? LuaSnip.Opts.Expand Subset of opts accepted by `snip_expand`. ----@return boolean _ Whether a snippet was expanded. -function API.expand(opts) +--- Expand a snippet in the current buffer. +---@param snippet LuaSnip.Snippet The snippet. +---@param opts? LuaSnip.Opts.SnipExpand Optional additional arguments. +---@return LuaSnip.ExpandedSnippet _ The snippet that was inserted into the +--- buffer. +function API.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 expand_params local snip -- find snip via next_expand (set from previous expandable()) or manual matching. @@ -629,7 +682,7 @@ function API.expand(opts) } -- override snip with expanded copy. - snip = API.snip_expand(snip, { + snip = _snip_expand(snip, { expand_params = expand_params, -- clear trigger-text. clear_region = clear_region, @@ -641,48 +694,61 @@ function API.expand(opts) return false end +--- Find a snippet whose trigger matches the text before the cursor and expand +--- it. +---@param opts? LuaSnip.Opts.Expand Subset of opts accepted by `snip_expand`. +---@return boolean _ Whether a snippet was expanded. +function API.expand(opts) + return api_do(_expand, opts) +end + --- Find an autosnippet matching the text at the cursor-position and expand it. function API.expand_auto() - local snip, expand_params = - match_snippet(util.get_current_line_to_cursor(), "autosnippets") - if snip then - assert(expand_params) -- hint lsp type checker - 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 = API.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 --- Repeat the last performed `snip_expand`. Useful for dot-repeat. function API.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 - API.snip_expand(session.last_expand_snip, session.last_expand_opts) + _snip_expand(session.last_expand_snip, session.last_expand_opts) + end) end --- Expand at the cursor, or jump forward. ---@return boolean _ Whether an action was performed. function API.expand_or_jump() - if API.expand() then - return true - end - if API.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 --- Expand a snippet specified in lsp-style. @@ -692,7 +758,7 @@ end --- `snip_expand`. function API.lsp_expand(body, opts) -- expand snippet as-is. - API.snip_expand( + api_do(_snip_expand, ls.parser.parse_snippet( "", body, @@ -728,20 +794,17 @@ local function safe_choice_action(snip, ...) end end ---- Change the currently active choice. ----@param val 1|-1 Move one choice forward or backward. -function API.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, @@ -750,22 +813,28 @@ function API.change_choice(val, opts) restore_data ) session.current_nodes[vim.api.nvim_get_current_buf()] = new_active - active_update_dependents() + _active_update_dependents() end ---- Set the currently active choice. ----@param choice_indx integer Index of the choice to switch to. -function API.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) +--- Change the currently active choice. +---@param val 1|-1 Move one choice forward or backward. +function API.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, @@ -774,7 +843,13 @@ function API.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 + +--- Set the currently active choice. +---@param choice_indx integer Index of the choice to switch to. +function API.set_choice(choice_indx, opts) + api_do(_set_choice, choice_indx, opts) end --- Get a string-representation of all the current choiceNode's choices. @@ -796,7 +871,7 @@ end --- Update all nodes that depend on the currently-active node. function API.active_update_dependents() - update_dependents(session.current_nodes[vim.api.nvim_get_current_buf()]) + api_do(_active_update_dependents) end --- Generate and store the docstrings for a list of snippets as generated by @@ -1295,7 +1370,10 @@ API.log = require("luasnip.util.log") ---@class LuaSnip: LuaSnip.API, LuaSnip.LazyAPI ls = lazy_table(API, ls_lazy) --- internal stuff, e.g. for tests. -ls.no_region_check_wrap = no_region_check_wrap +-- undocumented, internally-used, exported functions. +ls._api_do = api_do +ls._api_enter = api_enter +ls._api_leave = api_leave +ls._set_choice = _set_choice return ls diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index f552ac3e8..71afbcdaf 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -332,7 +332,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 b8933de1f..c6c347584 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -278,20 +278,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 @@ -313,7 +299,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) @@ -662,6 +648,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 d24b0c222..eab9cc1cb 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -198,6 +198,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 ef6a75474..36c215bd7 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -64,7 +64,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 32bedd5f9..63fafb7be 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -216,3 +216,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 e1cdc643b83a604c64306849667a78c847bbb2c0 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:15:10 +0100 Subject: [PATCH 62/77] 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 d996d5fc3..4089158e2 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -38,7 +38,14 @@ end 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 3a45291b8954551b383bc69314304036ad426ebc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:15:59 +0100 Subject: [PATCH 63/77] 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 decab9ef433a6bf31a9bbd4143e8ef5eb1a76ed7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:16:59 +0100 Subject: [PATCH 64/77] 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 3501f750964c10b4e059b3e50eff3c38c2c07278 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 23:01:24 +0200 Subject: [PATCH 65/77] 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 4089158e2..625f53079 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -201,7 +201,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 = @@ -318,7 +318,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. @@ -326,7 +326,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() @@ -335,8 +335,8 @@ local function update_dependents(node) 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 -- return next active node. @@ -804,20 +804,32 @@ 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() @@ -825,18 +837,28 @@ end --- Change the currently active choice. ---@param val 1|-1 Move one choice forward or backward. -function API.change_choice(val, opts) - api_do(_change_choice, val, opts) +function API.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") @@ -847,7 +869,9 @@ 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() @@ -855,8 +879,8 @@ end --- Set the currently active choice. ---@param choice_indx integer Index of the choice to switch to. -function API.set_choice(choice_indx, opts) - api_do(_set_choice, choice_indx, opts) +function API.set_choice(choice_indx) + api_do(_set_choice, choice_indx, {}) end --- Get a string-representation of all the current choiceNode's choices. @@ -1378,6 +1402,7 @@ API.log = require("luasnip.util.log") ls = lazy_table(API, ls_lazy) -- undocumented, internally-used, exported functions. +ls._active_update_dependents = _active_update_dependents ls._api_do = api_do ls._api_enter = api_enter ls._api_leave = api_leave From cb16ac0a6df48cfc699bf7fcff759a6f8190ada7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Thu, 14 Nov 2024 12:20:02 +0100 Subject: [PATCH 66/77] 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 d9fd54d0a..1146cb135 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -522,14 +522,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 1fdf6aced..ffd5846ad 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -426,4 +426,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 1ad0b62ab..01c6894c1 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -461,7 +461,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 e1c276cab7f4bdba27268e4aea064ac896b8c970 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 23:02:55 +0200 Subject: [PATCH 67/77] 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 625f53079..a45b15c44 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -39,12 +39,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 @@ -68,7 +70,6 @@ local function api_do(fn, ...) return fn_res end - -- returns matching snippet (needs to be copied before usage!) and its expand- -- parameters(trigger and captures). params are returned here because there's -- no need to recalculate them. @@ -201,7 +202,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 = @@ -227,7 +232,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 } @@ -247,7 +255,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 @@ -272,22 +281,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 @@ -303,7 +322,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 } @@ -326,17 +348,25 @@ 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 -- return next active node. @@ -765,7 +795,8 @@ end --- `snip_expand`. function API.lsp_expand(body, opts) -- expand snippet as-is. - api_do(_snip_expand, + api_do( + _snip_expand, ls.parser.parse_snippet( "", body, @@ -810,7 +841,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()] @@ -829,7 +862,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() @@ -848,7 +885,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()] @@ -871,7 +910,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 71afbcdaf..1331e644c 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -217,10 +217,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) @@ -324,7 +321,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 @@ -332,7 +329,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 c6c347584..e88e304b6 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -277,7 +277,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 @@ -584,17 +590,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 eab9cc1cb..9e07e7cfa 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -166,7 +166,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. @@ -174,7 +174,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 @@ -202,7 +202,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 @@ -213,12 +213,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. @@ -227,7 +227,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. @@ -238,11 +238,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 f63900d32..144dc71d2 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -419,7 +419,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`. @@ -427,7 +427,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) @@ -485,5 +488,5 @@ return { default_tbl_get = default_tbl_get, 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 1146cb135..b46c4a68b 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -310,7 +310,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"), @@ -320,10 +322,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", { @@ -338,24 +343,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() @@ -373,7 +379,7 @@ screen:expect({ [3] = { background = Screen.colors.LightGray }, }) - exec_lua[=[ + exec_lua([=[ ls.snip_expand(s("trig", { c(1, { fmt([[ @@ -388,13 +394,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 | @@ -402,11 +408,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 | @@ -414,12 +420,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} | @@ -427,11 +433,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} | @@ -439,11 +445,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}) }), @@ -452,9 +458,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 | | @@ -462,11 +468,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 | | @@ -474,12 +480,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 | ^ | @@ -487,11 +493,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 | @@ -499,12 +505,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}) }), @@ -513,20 +519,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( @@ -540,20 +546,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 }) @@ -571,35 +577,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, { @@ -614,54 +620,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 ffd5846ad..6bbbccc07 100644 --- a/tests/integration/dynamic_spec.lua +++ b/tests/integration/dynamic_spec.lua @@ -382,26 +382,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 @@ -413,22 +416,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 @@ -438,23 +443,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 01c6894c1..2e42c0ec4 100644 --- a/tests/integration/restore_spec.lua +++ b/tests/integration/restore_spec.lua @@ -366,25 +366,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", { @@ -396,32 +395,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", { @@ -433,7 +431,7 @@ screen:expect({ end, {1}) })) ]]) - exec_lua[[ls.jump(1)]] + exec_lua([[ls.jump(1)]]) feed(". .") exec_lua([[ ls.snip_expand(s("trig", { @@ -441,28 +439,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", { @@ -474,7 +471,7 @@ screen:expect({ end, {1}) })) ]]) - exec_lua[[ls.jump(1)]] + exec_lua([[ls.jump(1)]]) local function exp() exec_lua([[ @@ -486,66 +483,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 13740b0da..6bf096707 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -385,11 +385,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") @@ -2291,8 +2291,8 @@ describe("session", function() change(1) change(1) -- currently wrong! -screen:expect({ - grid = [[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -2323,7 +2323,7 @@ screen:expect({ {0:~ }| {0:~ }| {2:-- INSERT --} | - ]] -}) + ]], + }) end) end) diff --git a/tests/unit/str_spec.lua b/tests/unit/str_spec.lua index 63fafb7be..95379257a 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -203,18 +203,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() @@ -224,33 +256,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() @@ -260,28 +307,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) From f047e41b9fd3a60dd63ddab3e6f19e18977c9af1 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 5 May 2025 13:34:06 +0200 Subject: [PATCH 68/77] fix: pass correct arguments to str_byteindex. --- lua/luasnip/util/str.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 9e07e7cfa..1dbb90acd 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -224,9 +224,11 @@ function M.multiline_to_byte_offset(str, pos) -- 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 + -- I think we can always assume utf-8? + byte_pos = byte_pos + vim.str_byteindex(pos_line_str, "utf-8", pos[2]) + + -- 0- to 1-based columns. return byte_pos + 1 end From 8e92df9881c27ffcbc127f66eb73e0869f338ee2 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Mon, 5 May 2025 17:54:26 +0200 Subject: [PATCH 69/77] fix: handle unicode->snippetstring->unicode conversion correctly. --- lua/luasnip/util/str.lua | 22 +++--- tests/integration/choice_spec.lua | 111 ++++++++++++++++++++---------- tests/unit/str_spec.lua | 6 ++ 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 1dbb90acd..80dfdd239 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -199,8 +199,11 @@ function M.multiline_append(strmod, strappend) 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 +-- given in bytes and 0-based) into an offset (in bytes, 1-based) for -- the \n-concatenated version of that string. +--- +---@param str string[], a multiline string +---@param pos LuaSnip.ApiPosition, an api-position relative to the start of str. 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) @@ -218,21 +221,21 @@ function M.multiline_to_byte_offset(str, pos) -- 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 + if pos[2] >= #str[pos[1]+1] + 1 then -- in this case, pos is outside of the multiline-region. return nil end -- I think we can always assume utf-8? - byte_pos = byte_pos + vim.str_byteindex(pos_line_str, "utf-8", pos[2]) + byte_pos = byte_pos + 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. +-- inverse of multiline_to_byte_offset, 1-based byte to 0,0-based row,column. +---@param str string[], the multiline string +---@param byte_pos number, a 1-based index into the \n-concatenated `str`. function M.byte_to_multiline_offset(str, byte_pos) if byte_pos < 0 then return nil @@ -240,12 +243,11 @@ function M.byte_to_multiline_offset(str, byte_pos) local byte_pos_so_far = 0 for i, line in ipairs(str) do + -- line-length + \n. 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 } + -- byte is in this line, return it. + return { i - 1, byte_pos - byte_pos_so_far - 1 } end byte_pos_so_far = line_i_end end diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index b46c4a68b..bc534f0d8 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -370,8 +370,7 @@ describe("ChoiceNode", function() ls_helpers.clear() ls_helpers.session_setup_luasnip() - screen = Screen.new(50, 7) - screen:attach() + screen = ls_helpers.new_screen(50, 7) screen:set_default_attr_ids({ [0] = { bold = true, foreground = Screen.colors.Blue }, [1] = { bold = true, foreground = Screen.colors.Brown }, @@ -604,7 +603,7 @@ describe("ChoiceNode", function() screen:expect({ unchanged = true }) -- test some more wild stuff, just because. - feed(" ") + feed("") exec_lua([[ ls.snip_expand(s("trig", { t":", @@ -622,52 +621,88 @@ describe("ChoiceNode", function() })) ]]) - screen:expect({ - grid = [[ - :.ccee e :^c{3:c}eeee:eee ee.: | - {0:~ }| - {2:-- SELECT --} | - ]], - }) +screen:expect([[ + :.ccee ee :^c{3:c}eeee: ee 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 --} | - ]], - }) + +screen:expect([[ + :.ccee ee :.ccee ee^ee ee.: ee 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 --} | - ]], - }) +screen:expect([[ + :.cc^e{3:e ee :.ccee eeee ee.: ee 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 --} | - ]], - }) +screen:expect([[ + :cc^e{3:e ee :.ccee eeee ee.: ee 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 --} | - ]], - }) +screen:expect([[ + :ccee ee :.cc^e{3:e eeee ee}.: ee ee: | + {0:~ }| + {2:-- SELECT --} | +]]) + end) + + it("correctly handles unicode when storing and restoring.", function() + exec_lua([=[ + ls.snip_expand( + s("choice", { + c(1, { + {t"a ", r(1, "k", i(1)), t" a"}, + {t"bb ", r(1, "k"), t" bb"} + }, {restore_cursor = true}) + })) + ]=]) +screen:expect([[ + a ^ a | + {0:~ }| + {2:-- INSERT --} | +]]) + feed("a a") + exec_lua([=[ + ls.snip_expand(s("bad", {i(1, "i…i")})) + ]=]) +screen:expect([[ + a a ^i{3:…i} a a | + {0:~ }| + {2:-- SELECT --} | +]]) + + exec_lua("ls.change_choice(1)") +screen:expect([[ + bb a ^i{3:…i} a bb | + {0:~ }| + {2:-- SELECT --} | +]]) + feed("la") +screen:expect([[ + bb a i…^i a bb | + {0:~ }| + {2:-- INSERT --} | +]]) + exec_lua("ls.change_choice(1)") +screen:expect([[ + a a i…^i a a | + {0:~ }| + {2:-- INSERT --} | +]]) end) end) diff --git a/tests/unit/str_spec.lua b/tests/unit/str_spec.lua index 95379257a..e39060898 100644 --- a/tests/unit/str_spec.lua +++ b/tests/unit/str_spec.lua @@ -298,6 +298,9 @@ describe("str.multiline_to_byte_offset", function() 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("unicode1", { "aa … aa" }, { 0, 6 }, 7) + check("unicode2", { "aa …a… aa" }, { 0, 6 }, 7) + check("unicode3", { "aa …a… aa", "aa …a… aa" }, { 1, 6 }, 21) end) describe("byte_to_multiline_offset", function() @@ -342,4 +345,7 @@ describe("byte_to_multiline_offset", function() 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("unicode1", { "aa … aa" }, 7, { 0, 6 }) + check("unicode2", { "aa …a… aa" }, 7, { 0, 6 }) + check("unicode3", { "aa …a… aa", "aa …a… aa" }, 21, { 1, 6 }) end) From 3e273b34fab9fac2711b4b4c341b37c4a4e2d3cd Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 14 Oct 2025 23:07:47 +0200 Subject: [PATCH 70/77] add a few annotations. Helps track invariants. --- lua/luasnip/_types.lua | 3 +-- lua/luasnip/nodes/node.lua | 7 +++++++ lua/luasnip/nodes/util.lua | 34 +++++++++++++++++++++++++++++++++- lua/luasnip/util/feedkeys.lua | 13 +++++++++++-- lua/luasnip/util/mark.lua | 1 + 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lua/luasnip/_types.lua b/lua/luasnip/_types.lua index 435b367c0..e179c8257 100644 --- a/lua/luasnip/_types.lua +++ b/lua/luasnip/_types.lua @@ -1,7 +1,6 @@ ---@alias LuaSnip.Cursor {[1]: number, [2]: number} ---- 0-based region within a single line ----@class LuaSnip.MatchRegion +---@class LuaSnip.MatchRegion 0-based region within a single line ---@field row integer 0-based row ---@field col_range { [1]: integer, [2]: integer } 0-based column range, from-in, to-exclusive diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index e88e304b6..8a5dc8d98 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -9,6 +9,13 @@ local opt_args = require("luasnip.nodes.optional_arg") local snippet_string = require("luasnip.nodes.util.snippet_string") ---@class LuaSnip.Node +---@field key? any Key to identify the node with. +---@field store_id? number May be set when the node is used to store/restore. +---A generic node. +---@field mark? LuaSnip.Mark The mark associated with this node. +---@field type number Identifies the type of the snippet. +---@field next LuaSnip.Node Link to the next node in jump-order. +---@field prev LuaSnip.Node Link to the previous node in jump-order. local Node = {} ---@alias LuaSnip.NodeExtOpts {["active"|"passive"|"visited"|"unvisited"|"snippet_passive"]: vim.api.keyset.set_extmark} diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 82407dc94..cb27d9ffd 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -859,7 +859,39 @@ local function str_args(args) end, args) end +---@class LuaSnip.SnippetCursorRestoreData +---This class holds data about the current position of the cursor in a snippet. +---@field key string key of the current node. +---@field store_id number uniquely identifies the data associated with this +---store-restore cycle. +---This is necessary because eg. the snippetStrings may contain cursor-positions +---of more than one restore data, and the correct ones can be identified via +---store_id. +---@field node LuaSnip.Node The node the cursor will be stored relative to. +---@field cursor_start_relative LuaSnip.BytecolBufferPosition The position of +---the cursor, or beginning of selected area, relative to the beginning of +---`node`. +---@field selection_end_start_relative LuaSnip.BytecolBufferPosition The +---position of the cursor, or end of selected area, relative to the beginning of +---`node`. The column is one beyond the byte where the selection ends. +---@field mode string The first character (see `vim.fn.mode()`) of the mode at +---the time of `store`. + +---@alias LuaSnip.CursorRestoreData table +---Represents the position of the cursor relative to all snippets the cursor was +---inside. +---Maps a `store_id` to the data needed to restore the cursor relative to the +---stored node of that snippet. +---We need the data relative to all parent-snippets of some node because the +---first 1,2,... snippets may disappear when a choice is changed. + +---@class LuaSnip.StoreCursorNodeRelativeOpts +---@field place_cursor_mark boolean? Whether to, if possible, place a mark in +---snippetText. + local store_id = 0 +---@param node LuaSnip.Node The node to store the cursor relative to. +---@param opts LuaSnip.StoreCursorNodeRelativeOpts local function store_cursor_node_relative(node, opts) local data = {} @@ -936,7 +968,7 @@ local function store_cursor_node_relative(node, opts) -- 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 str = snippet_current_node:get_text() --[=[@as string[] ]=] local pos_byte_offset = str_util.multiline_to_byte_offset( str, snip_data.cursor_start_relative diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index cfaf9dc56..076068123 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -200,8 +200,17 @@ function M.confirm(id) end end --- if there are some operations that move the cursor enqueud, retrieve their --- target-state, otherwise return the current cursor state. +---@class LuaSnip.Feedkeys.LastState +---@field pos LuaSnip.BytecolBufferPosition Position of the cursor or beginning of visual +---area. +---@field pos_v LuaSnip.BytecolBufferPosition Position of the cursor or end of visual +---area. +---@field mode string Represents the current mode. Only the first character of +---`vim.fn.mode()`, so not completely exact. + +---if there are some operations that move the cursor enqueued, retrieve their +---target-state, otherwise return the current cursor state. +---@return LuaSnip.Feedkeys.LastState function M.last_state() if enqueued_cursor_state then local state = vim.deepcopy(enqueued_cursor_state) diff --git a/lua/luasnip/util/mark.lua b/lua/luasnip/util/mark.lua index 132913240..ab4e539f7 100644 --- a/lua/luasnip/util/mark.lua +++ b/lua/luasnip/util/mark.lua @@ -1,6 +1,7 @@ local session = require("luasnip.session") local util = require("luasnip.util.util") +---@class LuaSnip.Mark local Mark = {} function Mark:new(o) From 829bcfa281bd6a947854551328d14ef8fafc93f2 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Fri, 16 May 2025 10:07:55 +0200 Subject: [PATCH 71/77] feedkeys: only clear action after its confirm is called. --- lua/luasnip/util/feedkeys.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 076068123..0b5a362d0 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -25,6 +25,10 @@ local executing_id = nil local enqueued_actions = {} local enqueued_cursor_state +---Inserts keys into the beginning of the typeahead buffer and add a `confirm` +---after them. +---@param id number Id from next_id() +---@param keys string Keys to insert local function _feedkeys_insert(id, keys) executing_id = id vim.api.nvim_feedkeys( @@ -188,6 +192,7 @@ end function M.confirm(id) executing_id = nil + enqueued_actions[id] = nil if enqueued_cursor_state and enqueued_cursor_state.id == id then -- only clear state if set by this action. @@ -196,7 +201,6 @@ function M.confirm(id) if enqueued_actions[id + 1] then enqueued_actions[id + 1](id + 1) - enqueued_actions[id + 1] = nil end end From d37628ed5caac5ae6a6389412d6097f18720f2fc Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Jun 2025 18:48:28 +0200 Subject: [PATCH 72/77] store: check that child-snip has valid extmarks before storing. --- lua/luasnip/nodes/insertNode.lua | 38 +++++++++++++++++++------------- lua/luasnip/nodes/snippet.lua | 6 +++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 266e3783d..a379e4ba3 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -373,22 +373,30 @@ function InsertNode:get_snippetstring() 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 + -- it's possible that we first encounter a snippet with broken extmarks + -- in this loop, and those may cause errors when passed to + -- multiline_substr. + -- For now, simply treat it like regular text (`current` does not + -- advance -> the next append_text will include the text of the + -- snippet). + if snip:extmarks_valid() then + 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) ) - ) - current = snip_to_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 end snippetstring:append_text( str_util.multiline_substr( diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index dc9b9235a..0da7e6f4d 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1526,7 +1526,8 @@ function Snippet:extmarks_valid() return false end - -- below code does not work correctly if the snippet(Node) does not have any children. + -- the following code assumes that the snippet(Node) has at least one child, + -- if it doesn't, it's valid anyway. if #self.nodes == 0 then return true end @@ -1536,11 +1537,12 @@ function Snippet:extmarks_valid() pcall(node.mark.pos_begin_end_raw, node.mark) -- this snippet is invalid if: -- - we can't get the position of some node - -- - the positions aren't contiguous or don't completely fill the parent, or + -- - the positions aren't contiguous, don't completely fill the parent, or the `to` is before the `from`, or -- - any child of this node violates these rules. if not ok_ or util.pos_cmp(current_from, node_from) ~= 0 + or util.pos_cmp(node_from, node_to) > 0 or not node:extmarks_valid() then return false From 1d48d2f3573788696160a4df0146da0deed7376e Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 19 Oct 2025 11:58:51 +0200 Subject: [PATCH 73/77] improve logging of dynamicNode. --- lua/luasnip/nodes/dynamicNode.lua | 16 +++++++ lua/luasnip/nodes/node.lua | 5 +++ lua/luasnip/util/log.lua | 74 ++++++++++++++++++++++++++++++- lua/luasnip/util/table.lua | 6 +++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 3543b8342..469c2ef17 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -8,6 +8,8 @@ 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 log = require("luasnip.util.log").new("dynamicNode") +local describe = require("luasnip.util.log").describe local function D(pos, fn, args, opts) opts = opts or {} @@ -152,6 +154,11 @@ function DynamicNode:update() if vim.deep_equal(self.last_args, str_args) then -- no update, the args still match. + log.debug( + "skipping update of %s due to unchanged args (old: %s, new: %s)", + describe.node(self), + describe.inspect(self.last_args), + describe.inspect(str_args)) return end @@ -185,6 +192,7 @@ function DynamicNode:update() self.snip:exit() self.snip = nil + log.debug("content of %s before update: %s.", describe.node(self), describe.node_buftext(self)) -- focuses node. self:set_text_raw({ "" }) else @@ -246,6 +254,7 @@ function DynamicNode:update() local from, to = self.mark:pos_begin_end_raw() -- inserts nodes with extmarks false,false tmp:put_initial(from) + log.debug("content of %s after update: %s.", describe.node(self), describe.node_buftext(self)) -- adjust gravity in left side of snippet, such that it matches the current -- gravity of self. tmp:subtree_set_pos_rgrav(to, -1, true) @@ -434,6 +443,13 @@ function DynamicNode:update_restore() tmp:update_restore() else + log.debug( + "update_restore: rejecting stored data of %s (has snip: %s, snip is visible: %s, old args: %s, new args: %s)", + describe.node(self), + self.snip ~= nil, + self.snip and self.snip.visible, + describe.inspect(self.last_args), + describe.inspect(str_args)) self:update() end end diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 8a5dc8d98..d103b2fa7 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -7,6 +7,8 @@ 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 log = require("luasnip.util.log").new("node") +local describe = require("luasnip.util.log").describe ---@class LuaSnip.Node ---@field key? any Key to identify the node with. @@ -624,11 +626,14 @@ end -- self has to be visible/in the buffer. -- none of the node's ancestors may contain self. function Node:update_dependents(which) + log.debug("updating dependents of %s, selected %s", describe.node(self), describe.inspect(which, {newline =" ", indent = ""})) -- 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_restore() + else + log.debug("skipping update of %s, it is not visible.", describe.node(node)) end end end diff --git a/lua/luasnip/util/log.lua b/lua/luasnip/util/log.lua index 04e2883ea..8036e44bf 100644 --- a/lua/luasnip/util/log.lua +++ b/lua/luasnip/util/log.lua @@ -1,4 +1,5 @@ local util = require("luasnip.util.util") +local tbl = require("luasnip.util.table") -- older neovim-versions (even 0.7.2) do not have stdpath("log"). local logpath_ok, logpath = pcall(vim.fn.stdpath, "log") @@ -108,13 +109,84 @@ function M.set_loglevel(target_level) end end +local describe_key = {} + +local function mk_describe(f) + local wrapped_f = function(self) + return f(unpack(self.args)) + end + return function(...) + return { + args = {...}, + get = wrapped_f, + -- we want to be able to uniquely identify describe-objects, and the + -- simplest way (I think) is to set a unique key that is not known, + -- or even better, accessible by other modules. + [describe_key] = true + } + end +end +local function is_describe(t) + return type(t) == "table" and t[describe_key] ~= nil +end + +M.describe = { + node_buftext = mk_describe(function(node) + local from, to = node:get_buf_position() + return vim.inspect(vim.api.nvim_buf_get_text(0, from[1], from[2], to[1], to[2], {})) + end), + node = mk_describe(function(node) + if not node.parent then + return ("snippet[trig: %s]"):format(node.trigger) + else + local snip_id = node.parent.snippet.trigger + -- render node readably. + local node_id = "" + if node.key then + node_id = "key: " .. node.key + elseif node.absolute_insert_position then + node_id = "insert_pos: " .. vim.inspect(node.absolute_insert_position) + else + node_id = "pos: " .. vim.inspect(node.absolute_position) + end + return ("node[%s, snippet: `%s`]"):format(node_id, snip_id) + end + end), + inspect = mk_describe(function(t, inspect_opts) + return vim.inspect(t, inspect_opts or {}) + end), + traceback = mk_describe(function() + -- get position where log.debug is called with describe-object. + return debug.traceback("", 3) + end) +} + + +local function readable_format(msg, ...) + local args = tbl.pack(...) + for i, arg in ipairs(args) do + if is_describe(arg) then + args[i] = arg:get() + end + end + return msg:format(tbl.unpack(args)) +end + function M.new(module_name) local module_log = {} for name, _ in pairs(log) do module_log[name] = function(msg, ...) -- don't immediately get the referenced function, we'd like to -- allow changing the loglevel on-the-fly. - effective_log[name](module_name .. ": " .. msg:format(...)) + + -- also: make sure that whatever code called for logging does not + -- cause an error. + local ok, fmt_msg = pcall(readable_format, msg, ...) + if not ok then + effective_log.error(("log: error while formatting or writing message \"%s\": %s"):format(msg, fmt_msg)) + end + + effective_log[name](module_name .. ": " .. fmt_msg) end end return module_log diff --git a/lua/luasnip/util/table.lua b/lua/luasnip/util/table.lua index ba0876b5e..c41455729 100644 --- a/lua/luasnip/util/table.lua +++ b/lua/luasnip/util/table.lua @@ -33,7 +33,13 @@ local function list_to_set(values) return list end +-- http://lua-users.org/wiki/VarargTheSecondClassCitizen +local function pack2(...) return {n=select('#', ...), ...} end +local function unpack2(t) return unpack(t, 1, t.n) end + return { list_to_set = list_to_set, set_to_list = set_to_list, + pack = pack2, + unpack = unpack2 } From f04123863d62a988e0403cfd464b502a661b794c Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 19 Oct 2025 12:17:41 +0200 Subject: [PATCH 74/77] dynamicNode: limit number of nested updates. --- lua/luasnip/nodes/dynamicNode.lua | 68 ++++++++++++++++++++----------- lua/luasnip/session/init.lua | 2 + 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 469c2ef17..37e6f442d 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -10,6 +10,9 @@ local extend_decorator = require("luasnip.util.extend_decorator") local mark = require("luasnip.util.mark").mark local log = require("luasnip.util.log").new("dynamicNode") local describe = require("luasnip.util.log").describe +local session = require("luasnip.session") + +local update_depth_limit = 100 local function D(pos, fn, args, opts) opts = opts or {} @@ -167,9 +170,12 @@ function DynamicNode:update() end local tmp + local old_state = nil if self.snip then if not args then - -- a snippet exists, don't delete it. + -- a snippet exists, and we don't have data to update it => abort + -- update, keep existing snippet. + log.debug("skipping update of %s due to missing args.", describe.node(self)) return end @@ -181,14 +187,39 @@ function DynamicNode:update() self.snip:store() self.snip:subtree_leave_entered() - -- build new snippet before exiting, markers may be needed for construncting. - tmp = self.fn( - effective_args, - self.parent, - self.snip.old_state, - unpack(self.user_args) - ) + old_state = self.snip.old_state + end + + -- set `last_args` here, prevents additional updates after update_depth was + -- exceeded. + self.last_args = str_args + + local reset_depth = false + if not session.update_depths[self] then + session.update_depths[self] = 1 + reset_depth = true + elseif session.update_depths[self] < update_depth_limit then + session.update_depths[self] = session.update_depths[self] + 1 + elseif self.snip then + -- only skip updates if the snippet is already generated!! + log.error( + "Skipping update of %s because the number of nested updates exceeded %s. Traceback: %s", + describe.node(self), + update_depth_limit, + describe.traceback()) + return + end + + -- build new snippet before exiting, markers may be needed for + -- construncting. + tmp = self.fn( + effective_args, + self.parent, + old_state, + unpack(self.user_args) + ) + if self.snip then self.snip:exit() self.snip = nil @@ -196,24 +227,11 @@ function DynamicNode:update() -- focuses node. self:set_text_raw({ "" }) else + -- make sure dynamicNode is focused! self:focus() - if not args then - -- not all args are available => set to empty snippet. - tmp = SnippetNode(nil, {}) - else - -- also enter node here. - tmp = self.fn( - effective_args, - self.parent, - nil, - unpack(self.user_args) - ) - end end - -- 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 + log.debug("updating %s (depth: %s)", describe.node(self), session.update_depths[self]) -- act as if snip is directly inside parent. tmp.parent = self.parent @@ -272,6 +290,10 @@ function DynamicNode:update() -- 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 }) + + if reset_depth then + session.update_depths[self] = nil + end end local update_errorstring = [[ diff --git a/lua/luasnip/session/init.lua b/lua/luasnip/session/init.lua index 3522e2c41..866379d8a 100644 --- a/lua/luasnip/session/init.lua +++ b/lua/luasnip/session/init.lua @@ -55,4 +55,6 @@ function M.get_snip_env() return M.config.snip_env end +M.update_depths = {} + return M From 7fe81a9875a4cd6bc5ac459edde6dfa1d787aa50 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 19 Oct 2025 13:20:26 +0200 Subject: [PATCH 75/77] rip out update_depth again. I don't think that this is a good idea, at least currently, where I don't yet have a good feel for the way self-dependent dynamicNodes are going to be used. Difficulties: picking a good default depth. Even something as outwardly innocuous as 100 is bad when applied to a snippet that doubles some character. On the other hand, this can totally kill "chains" of updates, where some inner snippet updates text that is suddenly changed by an outer snippet... So, for now, I'm putting this down as user error, and even though having to kill your nvim-instance because it is caught in an infinite loop is not great at all, it shouldn't occur to often, and all ways around this I can think of may impede absolutely valid usage of dynamicNodes. --- lua/luasnip/nodes/dynamicNode.lua | 30 +++--------------------------- lua/luasnip/session/init.lua | 2 -- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 37e6f442d..a077ee514 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -12,8 +12,6 @@ local log = require("luasnip.util.log").new("dynamicNode") local describe = require("luasnip.util.log").describe local session = require("luasnip.session") -local update_depth_limit = 100 - local function D(pos, fn, args, opts) opts = opts or {} @@ -190,26 +188,6 @@ function DynamicNode:update() old_state = self.snip.old_state end - -- set `last_args` here, prevents additional updates after update_depth was - -- exceeded. - self.last_args = str_args - - local reset_depth = false - if not session.update_depths[self] then - session.update_depths[self] = 1 - reset_depth = true - elseif session.update_depths[self] < update_depth_limit then - session.update_depths[self] = session.update_depths[self] + 1 - elseif self.snip then - -- only skip updates if the snippet is already generated!! - log.error( - "Skipping update of %s because the number of nested updates exceeded %s. Traceback: %s", - describe.node(self), - update_depth_limit, - describe.traceback()) - return - end - -- build new snippet before exiting, markers may be needed for -- construncting. tmp = self.fn( @@ -231,7 +209,9 @@ function DynamicNode:update() self:focus() end - log.debug("updating %s (depth: %s)", describe.node(self), session.update_depths[self]) + log.debug("updating %s", describe.node(self)) + + self.last_args = str_args -- act as if snip is directly inside parent. tmp.parent = self.parent @@ -290,10 +270,6 @@ function DynamicNode:update() -- 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 }) - - if reset_depth then - session.update_depths[self] = nil - end end local update_errorstring = [[ diff --git a/lua/luasnip/session/init.lua b/lua/luasnip/session/init.lua index 866379d8a..3522e2c41 100644 --- a/lua/luasnip/session/init.lua +++ b/lua/luasnip/session/init.lua @@ -55,6 +55,4 @@ function M.get_snip_env() return M.config.snip_env end -M.update_depths = {} - return M From ffd2e53ddec8ffc584728ba6cd202913df5b9952 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 19 Oct 2025 11:29:46 +0000 Subject: [PATCH 76/77] 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 56a2b5999..96576120e 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2025 October 17 +*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2025 October 19 ============================================================================== Table of Contents *luasnip-table-of-contents* From 8eddf68179106f9b2529a6aca9428772755ea089 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Sun, 19 Oct 2025 11:29:49 +0000 Subject: [PATCH 77/77] Format with stylua --- lua/luasnip/init.lua | 15 +++++---------- lua/luasnip/nodes/dynamicNode.lua | 31 ++++++++++++++++++++----------- lua/luasnip/nodes/node.lua | 11 +++++++++-- lua/luasnip/util/log.lua | 21 ++++++++++++++------- lua/luasnip/util/str.lua | 2 +- lua/luasnip/util/table.lua | 10 +++++++--- tests/integration/choice_spec.lua | 20 ++++++++++---------- 7 files changed, 66 insertions(+), 44 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index a45b15c44..734c84b51 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -345,15 +345,11 @@ local function update_dependents(node, opts) -- 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, - cursor_restore_data = opts and opts.cursor_restore_data, - } - ) + 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, + }) if upd_res.new_current then upd_res.new_current:focus() session.current_nodes[vim.api.nvim_get_current_buf()] = @@ -684,7 +680,6 @@ function API.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`. diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index a077ee514..23c77f4be 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -159,7 +159,8 @@ function DynamicNode:update() "skipping update of %s due to unchanged args (old: %s, new: %s)", describe.node(self), describe.inspect(self.last_args), - describe.inspect(str_args)) + describe.inspect(str_args) + ) return end @@ -173,7 +174,10 @@ function DynamicNode:update() if not args then -- a snippet exists, and we don't have data to update it => abort -- update, keep existing snippet. - log.debug("skipping update of %s due to missing args.", describe.node(self)) + log.debug( + "skipping update of %s due to missing args.", + describe.node(self) + ) return end @@ -190,18 +194,18 @@ function DynamicNode:update() -- build new snippet before exiting, markers may be needed for -- construncting. - tmp = self.fn( - effective_args, - self.parent, - old_state, - unpack(self.user_args) - ) + tmp = + self.fn(effective_args, self.parent, old_state, unpack(self.user_args)) if self.snip then self.snip:exit() self.snip = nil - log.debug("content of %s before update: %s.", describe.node(self), describe.node_buftext(self)) + log.debug( + "content of %s before update: %s.", + describe.node(self), + describe.node_buftext(self) + ) -- focuses node. self:set_text_raw({ "" }) else @@ -252,7 +256,11 @@ function DynamicNode:update() local from, to = self.mark:pos_begin_end_raw() -- inserts nodes with extmarks false,false tmp:put_initial(from) - log.debug("content of %s after update: %s.", describe.node(self), describe.node_buftext(self)) + log.debug( + "content of %s after update: %s.", + describe.node(self), + describe.node_buftext(self) + ) -- adjust gravity in left side of snippet, such that it matches the current -- gravity of self. tmp:subtree_set_pos_rgrav(to, -1, true) @@ -447,7 +455,8 @@ function DynamicNode:update_restore() self.snip ~= nil, self.snip and self.snip.visible, describe.inspect(self.last_args), - describe.inspect(str_args)) + describe.inspect(str_args) + ) self:update() end end diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index d103b2fa7..40010ba50 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -626,14 +626,21 @@ end -- self has to be visible/in the buffer. -- none of the node's ancestors may contain self. function Node:update_dependents(which) - log.debug("updating dependents of %s, selected %s", describe.node(self), describe.inspect(which, {newline =" ", indent = ""})) + log.debug( + "updating dependents of %s, selected %s", + describe.node(self), + describe.inspect(which, { newline = " ", indent = "" }) + ) -- 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_restore() else - log.debug("skipping update of %s, it is not visible.", describe.node(node)) + log.debug( + "skipping update of %s, it is not visible.", + describe.node(node) + ) end end end diff --git a/lua/luasnip/util/log.lua b/lua/luasnip/util/log.lua index 8036e44bf..5692d8f83 100644 --- a/lua/luasnip/util/log.lua +++ b/lua/luasnip/util/log.lua @@ -117,12 +117,12 @@ local function mk_describe(f) end return function(...) return { - args = {...}, + args = { ... }, get = wrapped_f, -- we want to be able to uniquely identify describe-objects, and the -- simplest way (I think) is to set a unique key that is not known, -- or even better, accessible by other modules. - [describe_key] = true + [describe_key] = true, } end end @@ -133,7 +133,9 @@ end M.describe = { node_buftext = mk_describe(function(node) local from, to = node:get_buf_position() - return vim.inspect(vim.api.nvim_buf_get_text(0, from[1], from[2], to[1], to[2], {})) + return vim.inspect( + vim.api.nvim_buf_get_text(0, from[1], from[2], to[1], to[2], {}) + ) end), node = mk_describe(function(node) if not node.parent then @@ -145,7 +147,8 @@ M.describe = { if node.key then node_id = "key: " .. node.key elseif node.absolute_insert_position then - node_id = "insert_pos: " .. vim.inspect(node.absolute_insert_position) + node_id = "insert_pos: " + .. vim.inspect(node.absolute_insert_position) else node_id = "pos: " .. vim.inspect(node.absolute_position) end @@ -158,10 +161,9 @@ M.describe = { traceback = mk_describe(function() -- get position where log.debug is called with describe-object. return debug.traceback("", 3) - end) + end), } - local function readable_format(msg, ...) local args = tbl.pack(...) for i, arg in ipairs(args) do @@ -183,7 +185,12 @@ function M.new(module_name) -- cause an error. local ok, fmt_msg = pcall(readable_format, msg, ...) if not ok then - effective_log.error(("log: error while formatting or writing message \"%s\": %s"):format(msg, fmt_msg)) + effective_log.error( + ('log: error while formatting or writing message "%s": %s'):format( + msg, + fmt_msg + ) + ) end effective_log[name](module_name .. ": " .. fmt_msg) diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 80dfdd239..8f8a29ec8 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -221,7 +221,7 @@ function M.multiline_to_byte_offset(str, pos) -- allow positions one beyond the last character for all lines (even the -- last line). - if pos[2] >= #str[pos[1]+1] + 1 then + if pos[2] >= #str[pos[1] + 1] + 1 then -- in this case, pos is outside of the multiline-region. return nil end diff --git a/lua/luasnip/util/table.lua b/lua/luasnip/util/table.lua index c41455729..78e1aeeac 100644 --- a/lua/luasnip/util/table.lua +++ b/lua/luasnip/util/table.lua @@ -34,12 +34,16 @@ local function list_to_set(values) end -- http://lua-users.org/wiki/VarargTheSecondClassCitizen -local function pack2(...) return {n=select('#', ...), ...} end -local function unpack2(t) return unpack(t, 1, t.n) end +local function pack2(...) + return { n = select("#", ...), ... } +end +local function unpack2(t) + return unpack(t, 1, t.n) +end return { list_to_set = list_to_set, set_to_list = set_to_list, pack = pack2, - unpack = unpack2 + unpack = unpack2, } diff --git a/tests/integration/choice_spec.lua b/tests/integration/choice_spec.lua index bc534f0d8..b4f944181 100644 --- a/tests/integration/choice_spec.lua +++ b/tests/integration/choice_spec.lua @@ -621,7 +621,7 @@ describe("ChoiceNode", function() })) ]]) -screen:expect([[ + screen:expect([[ :.ccee ee :^c{3:c}eeee: ee ee.: | {0:~ }| {2:-- SELECT --} | @@ -630,7 +630,7 @@ screen:expect([[ feed("i aa ") exec_lua("ls.set_choice(2)") -screen:expect([[ + screen:expect([[ :.ccee ee :.ccee ee^ee ee.: ee ee.: | {0:~ }| {2:-- INSERT --} | @@ -641,20 +641,20 @@ screen:expect([[ exec_lua("ls.jump(-1)") exec_lua("ls.jump(-1)") exec_lua("ls.jump(1)") -screen:expect([[ + screen:expect([[ :.cc^e{3:e ee :.ccee eeee ee.: ee ee}.: | {0:~ }| {2:-- SELECT --} | ]]) exec_lua("ls.change_choice(1)") -screen:expect([[ + screen:expect([[ :cc^e{3:e ee :.ccee eeee ee.: ee ee}: | {0:~ }| {2:-- SELECT --} | ]]) exec_lua("ls.jump(1)") exec_lua("ls.jump(1)") -screen:expect([[ + screen:expect([[ :ccee ee :.cc^e{3:e eeee ee}.: ee ee: | {0:~ }| {2:-- SELECT --} | @@ -671,7 +671,7 @@ screen:expect([[ }, {restore_cursor = true}) })) ]=]) -screen:expect([[ + screen:expect([[ a ^ a | {0:~ }| {2:-- INSERT --} | @@ -680,26 +680,26 @@ screen:expect([[ exec_lua([=[ ls.snip_expand(s("bad", {i(1, "i…i")})) ]=]) -screen:expect([[ + screen:expect([[ a a ^i{3:…i} a a | {0:~ }| {2:-- SELECT --} | ]]) exec_lua("ls.change_choice(1)") -screen:expect([[ + screen:expect([[ bb a ^i{3:…i} a bb | {0:~ }| {2:-- SELECT --} | ]]) feed("la") -screen:expect([[ + screen:expect([[ bb a i…^i a bb | {0:~ }| {2:-- INSERT --} | ]]) exec_lua("ls.change_choice(1)") -screen:expect([[ + screen:expect([[ a a i…^i a a | {0:~ }| {2:-- INSERT --} |