diff --git a/DOC.md b/DOC.md index 8a205caff..466469119 100644 --- a/DOC.md +++ b/DOC.md @@ -28,6 +28,7 @@ local i = ls.insert_node local f = ls.function_node local c = ls.choice_node local d = ls.dynamic_node +local r = ls.restore_node local events = require("luasnip.util.events") ``` @@ -393,6 +394,82 @@ eg. 3, it would change to "3\nSample Text\nSample Text\nSample Text". Text that was inserted into any of the dynamicNodes insertNodes is kept when changing to a bigger number. +# RESTORENODE + +This node can store and restore a snippetNode that was modified (changed +choices, inserted text) by the user. It's usage is best demonstrated by an +example: + +```lua +s("paren_change", { + c(1, { + sn(nil, { t("("), r(1, "user_text"), t(")") }), + sn(nil, { t("["), r(1, "user_text"), t("]") }), + sn(nil, { t("{"), r(1, "user_text"), t("}") }), + }), +}, { + stored = { + user_text = i(1, "default_text") + } +}) +``` + +Here the text entered into `user_text` is preserved upon changing choice. + +The constructor for the restoreNode, `r`, takes (at most) three parameters: +- `pos`, when to jump to this node. +- `key`, the key that identifies which `restoreNode`s should share their + content. +- `nodes`, the contents of the `restoreNode`. Can either be a single node or + a table of nodes (both of which will be wrapped inside a `snippetNode`, + except if the single node already is a `snippetNode`). + The content of a given key may be defined multiple times, but if the + contents differ, it's undefined which will actually be used. + If a keys content is defined in a `dynamicNode`, it will not be used for + `restoreNodes` outside that `dynamicNode`. A way around this limitation is + defining the content in the `restoreNode` outside the `dynamicNode`. + +The content for a key may also be defined in the `opts`-parameter of the +snippet-constructor, as seen in the example above. The `stored`-table accepts +the same values as the `nodes`-parameter passed to `r`. +If no content is defined for a key, it defaults to the empty `insertNode`. + +The `restoreNode` is also useful for storing user-input across updates of a +`dynamicNode`. Consider this: + +```lua +local function simple_restore(args, _) + return sn(nil, {i(1, args[1]), i(2, "user_text")}) +end + +s("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) +}), +``` + +Every time the `i(1)` in the outer snippet is changed, the text inside the +`dynamicNode` is reset to `"user_text"`. This can be prevented by using a +`restoreNode`: + +```lua +local function simple_restore(args, _) + return sn(nil, {i(1, args[1]), r(2, "dyn", i(nil, "user_text"))}) +end + +ss("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) +}), +("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) +}), +``` +Now the entered text is stored. + +`RestoreNode`s indent is not influenced by `indentSnippetNodes` right now. If +that really bothers you feel free to open an issue. # EXTRAS diff --git a/Examples/snippets.lua b/Examples/snippets.lua index 1f8999b01..80988f454 100644 --- a/Examples/snippets.lua +++ b/Examples/snippets.lua @@ -7,8 +7,9 @@ local i = ls.insert_node local f = ls.function_node local c = ls.choice_node local d = ls.dynamic_node +local r = ls.restore_node local l = require("luasnip.extras").lambda -local r = require("luasnip.extras").rep +local rep = require("luasnip.extras").rep local p = require("luasnip.extras").partial local m = require("luasnip.extras").match local n = require("luasnip.extras").nonempty @@ -58,6 +59,9 @@ end -- complicated function for dynamicNode. local function jdocsnip(args, _, old_state) + -- !!! old_state is used to preserve user-input here. DON'T DO IT THAT WAY! + -- Using a restoreNode instead is much easier. + -- View this only as an example on how old_state functions. local nodes = { t({ "/**", " * " }), i(1, "A short Description"), @@ -200,12 +204,15 @@ ls.snippets = { -- Inside Choices, Nodes don't need a position as the choice node is the one being jumped to. sn(nil, { t("extends "), - i(1), + -- restoreNode: stores and restores nodes. + -- pass position, store-key and nodes. + r(1, "other_class", i(1)), t(" {"), }), sn(nil, { t("implements "), - i(1), + -- no need to define the nodes for a given key a second time. + r(1, "other_class"), t(" {"), }), }), @@ -309,7 +316,7 @@ ls.snippets = { i(0), }), -- Shorthand for repeating the text in a given node. - s("repeat", { i(1, "text"), t({ "", "" }), r(1) }), + s("repeat", { i(1, "text"), t({ "", "" }), rep(1) }), -- Directly insert the ouput from a function evaluated at runtime. s("part", p(os.date, "%Y")), -- use matchNodes to insert text based on a pattern/function/lambda-evaluation. @@ -391,9 +398,9 @@ ls.snippets = { ]], { i(1, "x"), - r(1), + rep(1), i(2, "y"), - r(2), + rep(2), } ) ), diff --git a/doc/luasnip.txt b/doc/luasnip.txt index 264e083ce..5a0e4efcc 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -12,16 +12,17 @@ CONTENTS *luasnip-content 6. SNIPPETNODE...............................................|luasnip-snippetnode| 7. INDENTSNIPPETNODE...................................|luasnip-indentsnippetnode| 8. DYNAMICNODE...............................................|luasnip-dynamicnode| -9. EXTRAS.........................................................|luasnip-extras| -10. LSP-SNIPPETS............................................|luasnip-lsp-snippets| -11. VARIABLES..................................................|luasnip-variables| -12. VSCODE SNIPPETS LOADER........................|luasnip-vscode_snippets_loader| -13. EXT_OPTS....................................................|luasnip-ext_opts| -14. DOCSTRING..................................................|luasnip-docstring| -15. DOCSTRING-CACHE......................................|luasnip-docstring-cache| -16. EVENTS........................................................|luasnip-events| -17. CLEANUP......................................................|luasnip-cleanup| -18. API-REFERENCE..........................................|luasnip-api-reference| +9. RESTORENODE...............................................|luasnip-restorenode| +10. EXTRAS........................................................|luasnip-extras| +11. LSP-SNIPPETS............................................|luasnip-lsp-snippets| +12. VARIABLES..................................................|luasnip-variables| +13. VSCODE SNIPPETS LOADER........................|luasnip-vscode_snippets_loader| +14. EXT_OPTS....................................................|luasnip-ext_opts| +15. DOCSTRING..................................................|luasnip-docstring| +16. DOCSTRING-CACHE......................................|luasnip-docstring-cache| +17. EVENTS........................................................|luasnip-events| +18. CLEANUP......................................................|luasnip-cleanup| +19. API-REFERENCE..........................................|luasnip-api-reference| > __ ____ /\ \ /\ _`\ __ @@ -51,6 +52,7 @@ All code-snippets in this help assume that local f = ls.function_node local c = ls.choice_node local d = ls.dynamic_node + local r = ls.restore_node local events = require("luasnip.util.events") < @@ -408,6 +410,80 @@ eg. 3, it would change to "3\nSample Text\nSample Text\nSample Text". Text that was inserted into any of the dynamicNodes insertNodes is kept when changing to a bigger number. +================================================================================ +RESTORENODE *luasnip-restorenode* + +This node can store and restore a snippetNode that was modified (changed +choices, inserted text) by the user. It's usage is best demonstrated by an +example: +> + s("paren_change", { + c(1, { + sn(nil, { t("("), r(1, "user_text"), t(")") }), + sn(nil, { t("["), r(1, "user_text"), t("]") }), + sn(nil, { t("{"), r(1, "user_text"), t("}") }), + }), + }, { + stored = { + user_text = i(1, "default_text") + } + }) +< + +Here the text entered into `user_text` is preserved upon changing choice. + +The constructor for the restoreNode, `r`, takes (at most) three parameters: +- `pos`, when to jump to this node. +- `key`, the key that identifies which `restoreNode`s should share their + content. +- `nodes`, the contents of the `restoreNode`. Can either be a single node or + a table of nodes (both of which will be wrapped inside a `snippetNode`, + except if the single node already is a `snippetNode`). + The content of a given key may be defined multiple times, but if the + contents differ, it's undefined which will actually be used. + If a keys content is defined in a `dynamicNode`, it will not be used for + `restoreNodes` outside that `dynamicNode`. A way around this limitation is + defining the content in the `restoreNode` outside the `dynamicNode`. + +The content for a key may also be defined in the `opts`-parameter of the +snippet-constructor, as seen in the example above. The `stored`-table accepts +the same values as the `nodes`-parameter passed to `r`. +If no content is defined for a key, it defaults to the empty `insertNode`. + +The `restoreNode` is also useful for storing user-input across updates of a +`dynamicNode`. Consider this: +> + local function simple_restore(args, _) + return sn(nil, {i(1, args[1]), i(2, "user_text")}) + end + s("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) + }), +< + +Every time the `i(1)` in the outer snippet is changed, the text inside the +`dynamicNode` is reset to `"user_text"`. This can be prevented by using a +`restoreNode`: +> + local function simple_restore(args, _) + return sn(nil, {i(1, args[1]), r(2, "dyn", i(nil, "user_text"))}) + end + ss("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) + }), + ("rest", { + i(1, "preset"), t{"",""}, + d(2, simple_restore, 1) + }), +< + +Now the entered text is stored. + +`RestoreNode`s indent is not influenced by `indentSnippetNodes` right now. If +that really bothers you feel free to open an issue. + ================================================================================ EXTRAS *luasnip-extras* diff --git a/lua/luasnip/config.lua b/lua/luasnip/config.lua index 274de1e6f..a2f52de7c 100644 --- a/lua/luasnip/config.lua +++ b/lua/luasnip/config.lua @@ -60,6 +60,13 @@ local defaults = { -- not used! snippet_passive = { hl_group = "LuasnipSnippetSnippetPassive" }, }, + [types.restoreNode] = { + active = { hl_group = "LuasnipRestoreNodeActive" }, + passive = { hl_group = "LuasnipRestoreNodePassive" }, + snippet_passive = { + hl_group = "LuasnipRestoreNodeSnippetPassive", + }, + }, }, ext_base_prio = 200, ext_prio_increase = 7, diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 4eb0c21a2..a36f04764 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -438,6 +438,7 @@ ls = { i = require("luasnip.nodes.insertNode").I, c = require("luasnip.nodes.choiceNode").C, d = require("luasnip.nodes.dynamicNode").D, + r = require("luasnip.nodes.restoreNode").R, snippet = snip_mod.S, snippet_node = snip_mod.SN, parent_indexer = snip_mod.P, @@ -447,6 +448,7 @@ ls = { insert_node = require("luasnip.nodes.insertNode").I, choice_node = require("luasnip.nodes.choiceNode").C, dynamic_node = require("luasnip.nodes.dynamicNode").D, + restore_node = require("luasnip.nodes.restoreNode").R, parser = require("luasnip.util.parser"), config = require("luasnip.config"), snippets = { all = {} }, diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index f053ecdee..d9845ee76 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -169,14 +169,20 @@ function ChoiceNode:change_choice(dir) self.active_choice:store() -- tear down current choice. self.active_choice:input_leave() + self.active_choice:exit() + + -- store in old_choice, active_choice has to be disabled to prevent reading + -- from cleared mark in set_mark_rgrav (which will be called in + -- parent:set_text(self,...) a few lines below). + local old_choice = self.active_choice + self.active_choice = nil + -- clear text. self.parent:set_text(self, { "" }) - self.active_choice:exit() - -- stylua: ignore - self.active_choice = dir == 1 and self.active_choice.next_choice - or self.active_choice.prev_choice + self.active_choice = dir == 1 and old_choice.next_choice + or old_choice.prev_choice self.active_choice.mark = self.mark:copy_pos_gravs( vim.deepcopy(self.parent.ext_opts[self.active_choice.type].passive) @@ -218,7 +224,10 @@ end -- val_begin/end may be nil, in this case that gravity won't be changed. function ChoiceNode:set_mark_rgrav(rgrav_beg, rgrav_end) node.set_mark_rgrav(self, rgrav_beg, rgrav_end) - self.active_choice:set_mark_rgrav(rgrav_beg, rgrav_end) + -- may be set to temporarily in change_choice. + if self.active_choice then + self.active_choice:set_mark_rgrav(rgrav_beg, rgrav_end) + end end function ChoiceNode:set_ext_opts(name) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 3e8838bec..60a0b460e 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -120,15 +120,16 @@ function DynamicNode:update() self.snip.old_state, unpack(self.user_args) ) + self.snip:exit() + self.snip = nil + -- enters node. self.parent:set_text(self, { "" }) - self.snip:exit() else -- also enter node here. self.parent:enter_node(self.indx) tmp = self.fn(self.last_args, self.parent, nil, unpack(self.user_args)) end - self.snip = nil -- act as if snip is directly inside parent. tmp.parent = self.parent @@ -193,12 +194,18 @@ end function DynamicNode:update_restore() -- only restore snippet if arg-values still match. if self.snip and vim.deep_equal(self:get_args(), self.last_args) then - self.snip.mark = self.mark:copy_pos_gravs( + -- prevent entering the uninitialized snip in enter_node in a few lines. + local tmp = self.snip + self.snip = nil + + tmp.mark = self.mark:copy_pos_gravs( vim.deepcopy(self.parent.ext_opts[types.snippetNode].passive) ) self.parent:enter_node(self.indx) - self.snip:put_initial(self.mark:pos_begin_raw()) - self.snip:update_restore() + tmp:put_initial(self.mark:pos_begin_raw()) + tmp:update_restore() + + self.snip = tmp else self:update() end diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua new file mode 100644 index 000000000..4ccd4c2a8 --- /dev/null +++ b/lua/luasnip/nodes/restoreNode.lua @@ -0,0 +1,184 @@ +-- restoreNode is implemented similarly to dynamicNode, only that it gets the snippetNode not from some function, but from self.snip.stored[key]. + +local Node = require("luasnip.nodes.node").Node +local wrap_nodes_in_snippetNode = + require("luasnip.nodes.snippet").wrap_nodes_in_snippetNode +local RestoreNode = Node:new() +local types = require("luasnip.util.types") +local events = require("luasnip.util.events") +local util = require("luasnip.util.util") +local conf = require("luasnip.config") +local mark = require("luasnip.util.mark").mark + +local function R(pos, key, nodes) + -- don't create nested snippetNodes, unnecessary. + nodes = nodes and wrap_nodes_in_snippetNode(nodes) + + return RestoreNode:new({ + pos = pos, + key = key, + mark = nil, + snip = nodes, + type = types.restoreNode, + dependents = {}, + -- TODO: find out why it's necessary only for this node. + active = false, + }) +end + +function RestoreNode:exit() + 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() + self.active = false +end + +function RestoreNode:input_enter() + self.active = true + self.mark:update_opts(self.parent.ext_opts[self.type].active) + + self:event(events.enter) +end + +function RestoreNode:input_leave() + self:event(events.leave) + + self:update_dependents() + self.active = false + self.mark:update_opts(self.parent.ext_opts[self.type].passive) +end + +-- set snippetNode for this key here. +function RestoreNode:subsnip_init() + -- don't overwrite potentially stored snippetNode. + -- due to metatable, there will always be a node set, but only those set + -- by it (should) have the is_default set to true. + if self.parent.snippet.stored[self.key].is_default and self.snip then + self.parent.snippet.stored[self.key] = self.snip + end +end + +-- don't need these, will be done in put_initial and get_static/docstring. +function RestoreNode:indent(_) end + +function RestoreNode:expand_tabs(_) end + +-- will be called when before expansion but after snip.parent was initialized. +-- Get the actual snippetNode here. +function RestoreNode:put_initial(pos) + local tmp = self.parent.snippet.stored[self.key]:copy() + + -- act as if snip is directly inside parent. + tmp.parent = self.parent + tmp.indx = self.indx + + tmp.next = self + tmp.prev = self + + tmp.env = self.parent.env + tmp.ext_opts = tmp.ext_opts + or util.increase_ext_prio( + vim.deepcopy(self.parent.ext_opts), + conf.config.ext_prio_increase + ) + tmp.snippet = self.parent.snippet + tmp.dependents = self.dependents + + tmp:populate_argnodes() + tmp:subsnip_init() + + if vim.o.expandtab then + tmp:expand_tabs(util.tab_width()) + end + + -- correctly set extmark for node. + -- does not modify ext_opts[node.type]. + local mark_opts = vim.tbl_extend("keep", { + right_gravity = false, + end_right_gravity = false, + }, self.parent.ext_opts[types.snippetNode].passive) + + local old_pos = vim.deepcopy(pos) + tmp:put_initial(pos) + tmp.mark = mark(old_pos, pos, mark_opts) + + tmp:set_old_text() + + self.snip = tmp +end + +-- the same as DynamicNode. +function RestoreNode:jump_into(dir, no_move) + if self.active then + self:input_leave() + if dir == 1 then + return self.next:jump_into(dir, no_move) + else + return self.prev:jump_into(dir, no_move) + end + else + self:input_enter() + return self.snip:jump_into(dir, no_move) + end +end + +function RestoreNode:set_ext_opts(name) + self.mark:update_opts(self.parent.ext_opts[self.type][name]) + self.snip:set_ext_opts(name) +end + +function RestoreNode:update() + self.snip:update() +end + +local function snip_init(self, snip) + snip.parent = self.parent + snip.env = self.parent.env + + snip.ext_opts = util.increase_ext_prio( + vim.deepcopy(self.parent.ext_opts), + conf.config.ext_prio_increase + ) + snip.snippet = self.parent.snippet + snip:subsnip_init() +end + +function RestoreNode:get_static_text() + -- cache static_text, no need to recalculate function. + if not self.static_text then + local tmp = self.parent.snippet.stored[self.key] + snip_init(self, tmp) + self.static_text = tmp:get_static_text() + end + return self.static_text +end + +function RestoreNode:get_docstring() + if not self.docstring then + local tmp = self.parent.snippet.stored[self.key] + -- init correctly. + snip_init(self, tmp) + self.docstring = tmp:get_docstring() + end + return self.docstring +end + +function RestoreNode:set_mark_rgrav(val_begin, val_end) + Node.set_mark_rgrav(self, val_begin, val_end) + -- snip is set in put_initial, before calls to that set_mark_rgrav() won't be called. + self.snip:set_mark_rgrav(val_begin, val_end) +end + +function RestoreNode:store() end + +-- will be restored through other means. +function RestoreNode:update_restore() + self.snip:update_restore() +end + +return { + R = R, +} diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index a0970f05f..64b981550 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -20,6 +20,19 @@ local callbacks_mt = { end, } +-- declare SN here, is needed in metatable. +local SN + +local stored_mt = { + __index = function(table, key) + -- default-node is just empty text. + local val = SN(nil, { iNode.I(1) }) + val.is_default = true + rawset(table, key, val) + return val + end, +} + local Snippet = node_mod.Node:new() local Parent_indexer = {} @@ -55,6 +68,7 @@ function Snippet:init_nodes() or node.type == types.snippetNode or node.type == types.choiceNode or node.type == types.dynamicNode + or node.type == types.restoreNode then if node.pos then insert_nodes[node.pos] = node @@ -87,6 +101,25 @@ function Snippet:init_nodes() self:populate_argnodes() end +local function wrap_nodes_in_snippetNode(nodes) + if getmetatable(nodes) then + -- is a node, not a table. + if nodes.type ~= types.snippetNode then + -- is not a snippetNode. + + -- pos might have been nil, just set it correctly here. + nodes.pos = 1 + return SN(nil, { nodes }) + else + -- is a snippetNode, wrapping it twice is unnecessary. + return nodes + end + else + -- is a table of nodes. + return SN(nil, nodes) + end +end + local function init_opts(opts) opts = opts or {} @@ -95,6 +128,15 @@ local function init_opts(opts) setmetatable(opts.callbacks, callbacks_mt) opts.condition = opts.condition or true_func opts.show_condition = opts.show_condition or true_func + + -- return sn(t("")) for so-far-undefined keys. + opts.stored = setmetatable(opts.stored or {}, stored_mt) + + -- wrap non-snippetNode in snippetNode. + for key, nodes in pairs(opts.stored) do + opts.stored[key] = wrap_nodes_in_snippetNode(nodes) + end + return opts end @@ -138,6 +180,7 @@ local function S(context, nodes, opts) active = false, type = types.snippet, hidden = context.hidden, + stored = opts.stored, }) -- is propagated to all subsnippets, used to quickly find the outer snippet snip.snippet = snip @@ -157,7 +200,7 @@ local function S(context, nodes, opts) return snip end -local function SN(pos, nodes, opts) +function SN(pos, nodes, opts) opts = init_opts(opts) local snip = Snippet:new({ @@ -297,6 +340,11 @@ function Snippet:trigger_expand(current_node) self.env = Environ:new() self:subsnip_init() + -- at this point `stored` contains the snippetNodes that will actually + -- be used, indent them once here. + for _, node in pairs(self.stored) do + node:indent(self.indentstr) + end -- remove snippet-trigger, Cursor at start of future snippet text. util.remove_n_before_cur(#self.trigger) @@ -849,4 +897,5 @@ return { SN = SN, P = P, ISN = ISN, + wrap_nodes_in_snippetNode = wrap_nodes_in_snippetNode, } diff --git a/lua/luasnip/util/types.lua b/lua/luasnip/util/types.lua index 97fa26235..17abb59b3 100644 --- a/lua/luasnip/util/types.lua +++ b/lua/luasnip/util/types.lua @@ -7,6 +7,7 @@ return { dynamicNode = 6, snippet = 7, exitNode = 8, + restoreNode = 9, names = { "textNode", "insertNode", @@ -16,6 +17,7 @@ return { "dynamicNode", "snippet", "exitNode", + "restoreNode", }, names_pascal_case = { "TextNode", @@ -26,5 +28,6 @@ return { "DynamicNode", "Snippet", "ExitNode", + "RestoreNode", }, }