Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
176420f
implement optional argnodes.
L3MON4D3 Dec 28, 2023
bd09791
implement subtree_do, invokes callbacks on tree of nodes (in a snippet).
L3MON4D3 Mar 4, 2024
5fdf9d8
make resolve_position work for static snippets.
L3MON4D3 Mar 4, 2024
aa1b233
fix: make root_path work for snippets.
L3MON4D3 Mar 4, 2024
b5d2a34
implement subtree_leave_entered, for leaving only entered nodes.
L3MON4D3 May 5, 2025
b1f3492
overhaul snippet-updates.
L3MON4D3 Nov 3, 2025
b419d9a
make sure visible is set on -1-node.
L3MON4D3 Mar 10, 2024
83c7649
propery remove child-snippets when `:exit`ing.
L3MON4D3 Apr 23, 2024
61bf611
exitNode: use same update_dependents as all other nodes.
L3MON4D3 Apr 23, 2024
c295b3f
update after snip_expand.
L3MON4D3 Oct 14, 2025
719d55c
Format with stylua
L3MON4D3 Oct 14, 2025
09217d6
Make insertNode correctly handle static_text if it's a snippetString.
L3MON4D3 Oct 28, 2024
3be6031
allow using snippet_string as dynamicNode-args.
L3MON4D3 Oct 28, 2024
a35adb8
restoreNode,insertNode: propagate store.
L3MON4D3 Oct 29, 2024
d4abde8
dynamicNode.update: store snippet before evaluating fn.
L3MON4D3 Oct 29, 2024
4ca232e
dynamicNode.update: copy extmarks after focusing.
L3MON4D3 Oct 29, 2024
3225dd1
dynamicNode.update: do update_restore instead of update.
L3MON4D3 Oct 29, 2024
45df41e
restoreNode: don't store on exit, store should have been called before.
L3MON4D3 Oct 29, 2024
888ac5e
add some tests for new restoreNode-behaviour.
L3MON4D3 Oct 29, 2024
722251d
choiceNode: correctly refocus when current_node is in another snippet.
L3MON4D3 Oct 29, 2024
7d8140c
we don't want to go into adjacent snippetNodes, but land between them.
L3MON4D3 Oct 29, 2024
5441143
snippet: correctly propagate exit to child_snippets (and clear them).
L3MON4D3 Oct 29, 2024
b63fb1f
add another test for the new restoreNode.
L3MON4D3 Oct 29, 2024
d4b8110
store content of nested snippets before capturing argnode.
L3MON4D3 Oct 29, 2024
508cc3f
make sure marks are invalidated even for nested snippets.
L3MON4D3 Oct 29, 2024
d40fc8a
get_args: `store` only when calling in static mode.
L3MON4D3 Oct 29, 2024
5192bf9
snippetstring: store strings as \n-separated string.
L3MON4D3 Oct 14, 2025
87ad057
small refactor.
L3MON4D3 Oct 30, 2024
fc7e586
implement a few simple string-operations on snippetString.
L3MON4D3 Oct 30, 2024
c5cfc00
fix flakiness in test.
L3MON4D3 Oct 30, 2024
3f7ce76
update: try to find new active node in child-snippet.
L3MON4D3 Oct 30, 2024
befdc53
allow replacing parts of a snippetString with other text.
L3MON4D3 Nov 2, 2024
6ee05ef
implement gsub on snippetString.
L3MON4D3 Nov 2, 2024
de371f5
snippetstring.replace: fix substitution in textNode.
L3MON4D3 Nov 2, 2024
7c34402
fix switchup.
L3MON4D3 Nov 2, 2024
b9313e2
make in-place modifying functions private.
L3MON4D3 Nov 2, 2024
80be0c4
add :sub to snippetString.
L3MON4D3 Nov 2, 2024
bea1bb7
add `opt` for the optional argument.
L3MON4D3 Nov 3, 2024
5f476e4
correctly store+restore visual selection during update.
L3MON4D3 Nov 3, 2024
24edd78
fNode: always store result in static_text.
L3MON4D3 Nov 4, 2024
4d25da0
update_dependents: get cursor-position after queried movements.
L3MON4D3 Nov 4, 2024
25ef218
move the jump_active-check into the autocommand.
L3MON4D3 Oct 14, 2025
79b336c
optionally update a node differnt from the current node.
L3MON4D3 Oct 14, 2025
e0ee698
update_dependents: use update_restore by default.
L3MON4D3 Nov 4, 2024
79722c5
choiceNode: call update_dependents after routine is done completely.
L3MON4D3 Nov 4, 2024
67b99dc
move no_region_wrap back into main-module.
L3MON4D3 Oct 14, 2025
fcfb058
dynamicNode/restoreNode: don't destroy snip on exit.
L3MON4D3 Nov 4, 2024
5123fde
handle selection on first line and column of buffer with `before`.
L3MON4D3 Nov 4, 2024
ad85601
document imperfect behaviour asserted by test.
L3MON4D3 Nov 4, 2024
482f0bf
export optional_arg as opt for tests.
L3MON4D3 Nov 4, 2024
d96028f
set jump_active=false ASAP.
L3MON4D3 Nov 4, 2024
28c1300
choiceNode: explicitly set parent and pos for choices.
L3MON4D3 Nov 6, 2024
a8e340c
fix(dynamicNode): don't access .snip in update_static.
L3MON4D3 Nov 6, 2024
98ed158
dynamicNode: optionally use .snip to generate docstring.
L3MON4D3 Nov 6, 2024
acec69f
enqueue cursor-movement due to update in typeahead.
L3MON4D3 Nov 6, 2024
2c5300f
get_args: do (static_)visible-check in get_args, not get_static_text.
L3MON4D3 Nov 6, 2024
d69df12
test docstring-generation with self-dependent dynamicNode.
L3MON4D3 Nov 6, 2024
24b925e
properly restore cursor-position in set_choice.
L3MON4D3 Nov 7, 2024
d3a7709
change_choice: use cursor-restore system from update_dependents.
L3MON4D3 Oct 14, 2025
37b963a
snippet_string: add metadata and marks.
L3MON4D3 Nov 13, 2024
eed721c
store cursor-position in snippetString to more accurately restore it.
L3MON4D3 Oct 14, 2025
6bef3b3
api_enter: only log an error when called recursively.
L3MON4D3 Nov 14, 2024
77c5de4
feedkeys: ignore errors on asynchronous nvim_win_set_cursor.
L3MON4D3 Nov 14, 2024
4be52be
correctly restore self-dependent dynamicNode.
L3MON4D3 Nov 14, 2024
26d21ae
change/set/select_choice: update current node before modifying choice.
L3MON4D3 Oct 14, 2025
29c1ee2
add a few tests for previous changes.
L3MON4D3 Nov 14, 2024
a4dd915
Format with stylua
L3MON4D3 Oct 14, 2025
dcdf2ae
fix: pass correct arguments to str_byteindex.
L3MON4D3 May 5, 2025
02fd641
fix: handle unicode->snippetstring->unicode conversion correctly.
L3MON4D3 May 5, 2025
38c55fe
add a few annotations.
L3MON4D3 Oct 14, 2025
9e1d258
feedkeys: only clear action after its confirm is called.
L3MON4D3 May 16, 2025
b525bd3
store: check that child-snip has valid extmarks before storing.
L3MON4D3 Jun 3, 2025
ab81681
improve logging of dynamicNode.
L3MON4D3 Oct 19, 2025
5c2225e
dynamicNode: limit number of nested updates.
L3MON4D3 Oct 19, 2025
42fdd36
rip out update_depth again.
L3MON4D3 Oct 19, 2025
f5bd74d
add brief documentation for new features.
L3MON4D3 Nov 2, 2025
31c9e2b
Format with stylua
L3MON4D3 Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Format with stylua
  • Loading branch information
L3MON4D3 committed Nov 3, 2025
commit 719d55c4fd01299966e538d77f5ad9406bd3da28
44 changes: 31 additions & 13 deletions lua/luasnip/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -196,24 +197,23 @@ 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

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()

Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lua/luasnip/nodes/choiceNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions lua/luasnip/nodes/dynamicNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [[
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion lua/luasnip/nodes/functionNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [[
Expand Down
30 changes: 28 additions & 2 deletions lua/luasnip/nodes/insertNode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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({
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
12 changes: 10 additions & 2 deletions lua/luasnip/nodes/node.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions lua/luasnip/nodes/snippet.lua
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,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)
Expand Down Expand Up @@ -787,7 +786,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
Expand Down
5 changes: 2 additions & 3 deletions lua/luasnip/nodes/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -849,7 +848,7 @@ local function collect_dependents(node, which, static)
dependents_set[dep] = true
end
end,
static = static
static = static,
})
end

Expand Down Expand Up @@ -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,
}
30 changes: 30 additions & 0 deletions lua/luasnip/nodes/util/snippet_string.lua
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions lua/luasnip/util/str.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -171,6 +201,7 @@ local function pascalcase(str)
end
return pascalcased
end

M.vscode_string_modifiers = {
upcase = string.upper,
downcase = string.lower,
Expand Down
10 changes: 10 additions & 0 deletions lua/luasnip/util/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -465,4 +474,5 @@ return {
validate = validate,
str_utf32index = str_utf32index,
default_tbl_get = default_tbl_get,
pos_offset = pos_offset
}
4 changes: 3 additions & 1 deletion tests/integration/session_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading