From 1da43509a6160996b2e5bb4b5daa1a6bd3412d14 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Oct 2023 15:23:23 +0200 Subject: [PATCH 1/7] Overhaul snippet-insertion. Before this, upon expanding a new snippet, its jumps were linked up with the currently active node, no matter its position in the buffer. The most prominent negative side-effect of this is jumping all over the buffer if a snippet is not jumped "through" (ie. to its $0). This finally adresses this properly by inserting snippets into nodes according to their position in the buffer. Read the changes to `DOC.md` for more info. This commit also deprecates `history` in `setup`, prefer the new options `keep_roots`, `link_roots`, and `link_children`, as they are more readable. Still, it is very unlikely that compatibility with `history` will ever be completely removed, so no need to fret about this. --- DOC.md | 75 +- Examples/snippets.lua | 5 +- lua/luasnip/config.lua | 16 +- lua/luasnip/init.lua | 291 ++-- lua/luasnip/nodes/choiceNode.lua | 4 + lua/luasnip/nodes/dynamicNode.lua | 7 + lua/luasnip/nodes/insertNode.lua | 125 +- lua/luasnip/nodes/node.lua | 26 +- lua/luasnip/nodes/restoreNode.lua | 5 + lua/luasnip/nodes/snippet.lua | 379 ++++- lua/luasnip/nodes/textNode.lua | 12 + lua/luasnip/nodes/util.lua | 504 +++++- lua/luasnip/session/init.lua | 10 + lua/luasnip/util/util.lua | 19 + tests/helpers.lua | 22 +- tests/integration/session_spec.lua | 1745 +++++++++++++++++++++ tests/integration/snippet_basics_spec.lua | 123 +- 17 files changed, 3020 insertions(+), 348 deletions(-) create mode 100644 tests/integration/session_spec.lua diff --git a/DOC.md b/DOC.md index 7d8772b5c..257c6b333 100644 --- a/DOC.md +++ b/DOC.md @@ -118,6 +118,41 @@ ls.add_snippets("all", { It is possible to make snippets from one filetype available to another using `ls.filetype_extend`, more info on that in the section [API](#api-2). +## Snippet Insertion +When a new snippet is expanded, it can be connected with the snippets that have +already been expanded in the buffer in various ways. +First of all, Luasnip distinguishes between root-snippets and child-snippets. +The latter are nested inside other snippets, so when jumping through a snippet, +one may also traverse the child-snippets expanded inside it, more or less as if +the child just contains more nodes of the parent. +Root-snippets are of course characterised by not being child-snippets. +When expanding a new snippet, it becomes a child of the snippet whose region it +is expanded inside, and a root if it is not inside any snippet's region. +If it is inside another snippet, the specific node it is inside is determined, +and the snippet then nested inside that node. +* If that node is interactive (for example, an `insertNode`), the new snippet + will be traversed when the node is visited, as long as the + configuration-option `link_children` is enabled. If it is not enabled, it is + possible to jump from the snippet to the node, but not the other way around. +* If that node is not interactive, the snippet will be linked to the currently + active node, also such that it will not be jumped to again once it is left. + This is to prevent jumping large distances across the buffer as much as + possible. There may still be one large jump from the snippet back to the + current node it is nested inside, but that seems hard to avoid. + Thus, one should design snippets such that the regions where other snippets + may be expanded are inside `insertNodes`. + +If the snippet is not a child, but a root, it can be linked up with the roots +immediately adjacent to it by enabling `link_roots` in `setup`. +Since by default only one root is remembered, one should also set `keep_roots` +if `link_roots` is enabled. The two are separate options, since roots that are +not linked can still be reached by `ls.activate_node()`. This setup (remember +roots, but don't jump to them) is useful for a super-tab like mapping (`` +and jump on the same key), where one would like to still enter previous roots. +Since there would almost always be more jumps if the roots are linked, regular +`` would not work almost all the time, and thus `link_roots` has to stay +disabled. + # Node Every node accepts, as its last parameter, an optional table of arguments. @@ -3392,10 +3427,15 @@ It is also possible to get/set the source of a snippet via API: These are the settings you can provide to `luasnip.setup()`: -- `history`: If true, snippets that were exited can still be jumped back into. - As snippets are not removed when their text is deleted, they have to be - removed manually via `LuasnipUnlinkCurrent` if `delete_check_events` is not - enabled (set to eg. `'TextChanged'`). +- `keep_roots`: Whether snippet-roots should be linked. See + [Basics-Snippet-Insertion](#snippet-insertion) for more context. +- `link_roots`: Whether snippet-roots should be linked. See + [Basics-Snippet-Insertion](#snippet-insertion) for more context. +- `link_children`: Whether children should be linked. See + [Basics-Snippet-Insertion](#snippet-insertion) for more context. +- `history` (deprecated): if not nil, `keep_roots`, `link_roots`, and + `link_children` will bet set to the value of `history`. + This is just to ensure backwards-compatibility. - `update_events`: Choose which events trigger an update of the active nodes' dependents. Default is just `'InsertLeave'`, `'TextChanged,TextChangedI'` would update on every change. @@ -3406,8 +3446,8 @@ These are the settings you can provide to `luasnip.setup()`: update_events = {"TextChanged", "TextChangedI"} }) ``` -- `region_check_events`: Events on which to leave the current snippet if the - cursor is outside its' 'region'. Disabled by default, `'CursorMoved'`, +- `region_check_events`: Events on which to leave the current snippet-root if + the cursor is outside its' 'region'. Disabled by default, `'CursorMoved'`, `'CursorHold'` or `'InsertEnter'` seem reasonable. - `delete_check_events`: When to check if the current snippet was deleted, and if so, remove it from the history. Off by default, `'TextChanged'` (perhaps @@ -3679,14 +3719,11 @@ These are the settings you can provide to `luasnip.setup()`: returned. - `exit_out_of_region(node)`: checks whether the cursor is still within the - range of the snippet `node` belongs to. If yes, no change occurs; if no, the - snippet is exited and following snippets' regions are checked and potentially - exited (the next active node will be the 0-node of the snippet before the one - the cursor is inside. - If the cursor isn't inside any snippet, the active node will be the last node - in the jumplist). - If a jump causes an error (happens mostly because a snippet was deleted), the - snippet is removed from the jumplist. + range of the root-snippet `node` belongs to. If yes, no change occurs; if no, the + root-snippet is exited and its `$0` will be the new active node. + If a jump causes an error (happens mostly because the text of a snippet was + deleted), the snippet is removed from the jumplist and the current node set to + the end/beginning of the next/previous snippet. - `store_snippet_docstrings(snippet_table)`: Stores the docstrings of all snippets in `snippet_table` to a file @@ -3752,6 +3789,16 @@ These are the settings you can provide to `luasnip.setup()`: the destination could not be determined (most likely because there is no node that can be jumped to in the given direction, or there is no active node). +- `activate_node(opts)`: Activate a node in any snippet. + `opts` contains the following options: + * `pos`, `{[1]: row, [2]: byte-column}?`: The position at which a node should + be activated. Defaults to the position of the cursor. + * `strict`, `bool?`: If set, throw an error if the node under the cursor can't + be jumped into. If not set, fall back to any node of the snippet and enter + that instead. + * `select`, `bool?`: Whether the text inside the node should be selected. + Defaults to true. + Not covered in this section are the various node-constructors exposed by the module, their usage is shown either previously in this file or in `Examples/snippets.lua` (in the repo). diff --git a/Examples/snippets.lua b/Examples/snippets.lua index 512716ad2..c8cfa9c1f 100644 --- a/Examples/snippets.lua +++ b/Examples/snippets.lua @@ -25,7 +25,10 @@ local conds_expand = require("luasnip.extras.conditions.expand") -- Every unspecified option will be set to the default. ls.setup({ - history = true, + keep_roots = true, + link_roots = true, + link_children = true, + -- Update more often, :h events for more info. update_events = "TextChanged,TextChangedI", -- Snippets aren't automatically removed if their text is deleted. diff --git a/lua/luasnip/config.lua b/lua/luasnip/config.lua index f55aa2cc8..50ccddf3b 100644 --- a/lua/luasnip/config.lua +++ b/lua/luasnip/config.lua @@ -48,7 +48,11 @@ local lazy_snip_env = { } local defaults = { - history = false, + -- corresponds to legacy "history=false". + keep_roots = false, + link_roots = false, + link_children = false, + update_events = "InsertLeave", -- see :h User, event should never be triggered(except if it is `doautocmd`'d) region_check_events = nil, @@ -208,6 +212,16 @@ c = { set_snip_env(conf, user_config) + -- handle legacy-key history. + if user_config.history ~= nil then + conf.keep_roots = user_config.history + conf.link_roots = user_config.history + conf.link_children = user_config.history + + -- unset key to prevent handling twice. + conf.history = nil + end + for k, v in pairs(user_config) do conf[k] = v end diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 185bb94a4..b71389e5b 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -1,4 +1,7 @@ local util = require("luasnip.util.util") +local types = require("luasnip.util.types") +local node_util = require("luasnip.nodes.util") + local session = require("luasnip.session") local snippet_collection = require("luasnip.session.snippet_collection") local Environ = require("luasnip.util.environ") @@ -83,7 +86,48 @@ local function available(snip_info) return res end -local function safe_jump(node, dir, no_move, dry_run) +local unlink_set_adjacent_as_current +local function unlink_set_adjacent_as_current_no_log(snippet) + -- prefer setting previous/outer insertNode as current node. + local next_current = + -- either pick i0 of snippet before, or i(-1) of next snippet. + snippet.prev.prev or snippet:next_node() + snippet:remove_from_jumplist() + + if next_current then + -- if snippet was active before, we need to now set its parent to be no + -- longer inner_active. + if snippet.parent_node == next_current and next_current.inner_active then + snippet.parent_node:input_leave_children() + else + -- set no_move. + local ok, err = pcall(next_current.input_enter, next_current, true) + if not ok then + -- this won't try to set the previously broken snippet as + -- current, since that link is removed in + -- `remove_from_jumplist`. + unlink_set_adjacent_as_current(next_current.parent.snippet, "Error while setting adjacent snippet as current node: %s", err) + end + end + end + + session.current_nodes[vim.api.nvim_get_current_buf()] = next_current +end +function unlink_set_adjacent_as_current(snippet, reason, ...) + log.warn("Removing snippet %s: %s", snippet.trigger, reason:format(...)) + unlink_set_adjacent_as_current_no_log(snippet) +end +local function unlink_current() + local current = session.current_nodes[vim.api.nvim_get_current_buf()] + if not current then + print("No active Snippet") + return + end + unlink_set_adjacent_as_current_no_log() +end + +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 @@ -93,45 +137,24 @@ local function safe_jump(node, dir, no_move, dry_run) return res else local snip = node.parent.snippet - log.warn("Removing snippet `%s` due to error %s", snip.trigger, res) - - snip:remove_from_jumplist() - -- dir==1: try jumping into next snippet, then prev - -- dir==-1: try jumping into prev snippet, then next - if dir == 1 then - return safe_jump( - snip.next.next or snip.prev.prev, - snip.next.next and 1 or -1, - no_move, - dry_run - ) - else - return safe_jump( - snip.prev.prev or snip.next.next, - snip.prev.prev and -1 or 1, - no_move, - dry_run - ) - end + + unlink_set_adjacent_as_current(snip, "Removing snippet `%s` due to error %s", snip.trigger, res) + return session.current_nodes[vim.api.nvim_get_current_buf()] end end local function jump(dir) local current = session.current_nodes[vim.api.nvim_get_current_buf()] if current then session.current_nodes[vim.api.nvim_get_current_buf()] = - util.no_region_check_wrap(safe_jump, current, dir) + util.no_region_check_wrap(safe_jump_current, dir) return true else return false end end local function jump_destination(dir) - local current = session.current_nodes[vim.api.nvim_get_current_buf()] - if current then - -- dry run of jump (+no_move ofc.), only retrieves destination-node. - return safe_jump(current, dir, true, { active = {} }) - end - return nil + -- dry run of jump (+no_move ofc.), only retrieves destination-node. + return safe_jump_current(dir, true, { active = {} }) end local function jumpable(dir) @@ -149,20 +172,6 @@ local function expand_or_jumpable() return expandable() or jumpable(1) end -local function unlink_current() - local node = session.current_nodes[vim.api.nvim_get_current_buf()] - if not node then - print("No active Snippet") - return - end - local snippet = node.parent.snippet - - snippet:remove_from_jumplist() - -- prefer setting previous/outer insertNode as current node. - session.current_nodes[vim.api.nvim_get_current_buf()] = snippet.prev.prev - or snippet.next.next -end - local function in_snippet() -- check if the cursor on a row inside a snippet. local node = session.current_nodes[vim.api.nvim_get_current_buf()] @@ -173,11 +182,10 @@ local function in_snippet() local ok, snip_begin_pos, snip_end_pos = pcall(snippet.mark.pos_begin_end, snippet.mark) if not ok then - log.warn("Error while getting extmark-position: %s", snip_begin_pos) -- if there was an error getting the position, the snippets text was -- most likely removed, resulting in messed up extmarks -> error. -- remove the snippet. - unlink_current() + unlink_set_adjacent_as_current(snippet, "Error while getting extmark-position: %s", snip_begin_pos) return end local pos = vim.api.nvim_win_get_cursor(0) @@ -241,34 +249,28 @@ local function snip_expand(snippet, opts) ) end - snip:trigger_expand( + local snip_parent_node = snip:trigger_expand( session.current_nodes[vim.api.nvim_get_current_buf()], pos_id, env ) - local current_buf = vim.api.nvim_get_current_buf() - - if session.current_nodes[current_buf] then - local current_node = session.current_nodes[current_buf] - if current_node.pos > 0 then - -- snippet is nested, notify current insertNode about expansion. - current_node.inner_active = true - else - -- snippet was expanded behind a previously active one, leave the i(0) - -- properly (and remove the snippet on error). - local ok, err = pcall(current_node.input_leave, current_node) - if not ok then - log.warn("Error while leaving snippet: ", err) - current_node.parent.snippet:remove_from_jumplist() - end - end - end - -- jump_into-callback returns new active node. session.current_nodes[vim.api.nvim_get_current_buf()] = opts.jump_into_func(snip) + local buf_snippet_roots = session.snippet_roots[vim.api.nvim_get_current_buf()] + if not session.config.keep_roots and #buf_snippet_roots > 1 then + -- if history is not set, and there is more than one snippet-root, + -- remove the other one. + -- The nice thing is: since we maintain that #buf_snippet_roots == 1 + -- whenever outside of this function, we know that if we're here, it's + -- because this snippet was just inserted into buf_snippet_roots. + -- Armed with this knowledge, we can just check which of the roots is + -- this snippet, and remove the other one. + buf_snippet_roots[buf_snippet_roots[1] == snip and 2 or 1]:remove_from_jumplist() + end + -- stores original snippet, it doesn't contain any data from expansion. session.last_expand_snip = snippet session.last_expand_opts = opts @@ -390,14 +392,10 @@ local function safe_choice_action(snip, ...) if ok then return res else - log.warn("Removing snippet `%s` due to error %s", snip.trigger, res) - - snip:remove_from_jumplist() - return safe_jump( - -- jump to next or previous snippet. - snip.next.next or snip.prev.prev, - snip.next.next and 1 or -1 - ) + -- not very elegant, but this way we don't have a near + -- re-implementation of unlink_current. + unlink_set_adjacent_as_current(snip, "Removing snippet `%s` due to error %s", snip.trigger, res) + return session.current_nodes[vim.api.nvim_get_current_buf()] end end local function change_choice(val) @@ -466,23 +464,23 @@ local function active_update_dependents() local ok, err = pcall(active.update_dependents, active) if not ok then log.warn( + ) + unlink_set_adjacent_as_current(active.parent.snippet, "Error while updating dependents for snippet %s due to error %s", active.parent.snippet.trigger, - err - ) - unlink_current() + 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 - log.warn( + unlink_set_adjacent_as_current(active.parent.snippet, "Error while entering node in snippet %s: %s", active.parent.snippet.trigger, err ) - unlink_current() + return end @@ -574,31 +572,12 @@ local function unlink_current_if_deleted() return end local snippet = node.parent.snippet - local ok, snip_begin_pos, snip_end_pos = - pcall(snippet.mark.pos_begin_end_raw, snippet.mark) - - if not ok then - log.warn("Error while getting extmark-position: %s", snip_begin_pos) - end - - -- stylua: ignore - -- leave snippet if empty: - if not ok or - -- either exactly the same position... - (snip_begin_pos[1] == snip_end_pos[1] and - snip_begin_pos[2] == snip_end_pos[2]) or - -- or the end-mark is one line below and there is no text between them. - -- (this can happen when deleting linewise-visual or via `dd`) - (snip_begin_pos[1]+1 == snip_end_pos[1] and - snip_end_pos[2] == 0 and - - #vim.api.nvim_buf_get_lines(0, snip_begin_pos[1], snip_begin_pos[1]+1, true)[1] == 0) then - - log.info("Detected deletion of snippet `%s`, removing it", snippet.trigger) - snippet:remove_from_jumplist() - session.current_nodes[vim.api.nvim_get_current_buf()] = snippet.prev.prev - or snippet.next.next + -- extmarks_valid checks that + -- * textnodes that should contain text still do so, and + -- * that extmarks still fulfill all expectations (should be successive, no gaps, etc.) + if not snippet:extmarks_valid() then + unlink_set_adjacent_as_current(snippet, "Detected deletion of snippet `%s`, removing it", snippet.trigger) end end @@ -609,45 +588,57 @@ local function exit_out_of_region(node) end local pos = util.get_cursor_0ind() - local snippet = node.parent.snippet + local snippet + if node.type == types.snippet then + snippet = node + else + snippet = node.parent.snippet + end + + -- find root-snippet. + while snippet.parent_node do + snippet = snippet.parent_node.parent.snippet + end + local ok, snip_begin_pos, snip_end_pos = pcall(snippet.mark.pos_begin_end, snippet.mark) if not ok then - log.warn("Error while getting extmark-position: %s", snip_begin_pos) + unlink_set_adjacent_as_current(snippet, "Error while getting extmark-position: %s", snip_begin_pos) + return end -- stylua: ignore -- leave if curser before or behind snippet - if not ok or - pos[1] < snip_begin_pos[1] or + if pos[1] < snip_begin_pos[1] or pos[1] > snip_end_pos[1] then - -- jump as long as the 0-node of the snippet hasn't been reached. - -- check for nil; if history is not set, the jump to snippet.next - -- returns nil. - while node and node ~= snippet.next do - -- set no_move. - ok, node = pcall(node.jump_from, node, 1, true) - if not ok then - log.warn("Error while jumping from node: %s", node) - snippet:remove_from_jumplist() - -- may be nil, checked later. - node = snippet.next - break - end + -- make sure the snippet can safely be entered, since it may have to + -- be, in `refocus`. + if not snippet:extmarks_valid() then + unlink_set_adjacent_as_current(snippet, "Leaving snippet-root due to invalid extmarks.") + return end - session.current_nodes[vim.api.nvim_get_current_buf()] = node - -- also check next snippet. - if node and node.next then - if exit_out_of_region(node.next) then - node:input_leave(1, true) + local next_active = snippet.insert_nodes[0] + -- if there is a snippet nested into the $0, enter its $0 instead, + -- recursively. + -- This is to ensure that a jump forward after leaving the region of a + -- root will jump to the next root, or not result in a jump at all. + while next_active.inner_first do + -- make sure next_active is nested into completely intact + -- snippets, since that is a precondition on the to-node of + if not next_active.inner_first:extmarks_valid() then + next_active.inner_first:remove_from_jumplist() + else + -- inner_first is always the snippet, not the -1-node. + next_active = next_active.inner_first.insert_nodes[0] end end - return true + + node_util.refocus(node, next_active) + session.current_nodes[vim.api.nvim_get_current_buf()] = next_active end - return false end -- ft string, extend_ft table of strings. @@ -742,6 +733,57 @@ local function clean_invalidated(opts) snippet_collection.clean_invalidated(opts) end +local function activate_node(opts) + opts = opts or {} + local pos = opts.pos or util.get_cursor_0ind() + local strict = vim.F.if_nil(opts.strict, false) + local select = vim.F.if_nil(opts.select, true) + + -- find tree-node the snippet should be inserted at (could be before another node). + local _, _, _, node = node_util.snippettree_find_undamaged_node(pos, { + tree_respect_rgravs = false, + tree_preference = node_util.binarysearch_preference.inside, + snippet_mode = "interactive" + }) + + if not node then + error("No Snippet at that position") + return + end + + -- only activate interactive nodes, or nodes that are immediately nested + -- inside a choiceNode. + if not node:interactive() then + if strict then + error("Refusing to activate a non-interactive node.") + return + else + -- fall back to known insertNode. + -- snippet.insert_nodes[1] may be preferable, but that is not + -- certainly an insertNode (and does not even certainly contain an + -- insertNode, think snippetNode with only textNode). + -- We could *almost* find the first activateable node by + -- dry_run-jumping into the snippet, but then we'd also need some + -- mechanism for setting the active-state of all nodes to false, + -- which we don't yet have. + -- + -- Instead, just choose -1-node, and allow jumps from there, which + -- is much simpler. + node = node.parent.snippet.prev + end + end + + node_util.refocus(session.current_nodes[vim.api.nvim_get_current_buf()], node) + if select then + -- input_enter node again, to get highlight and the like. + -- One side-effect of this is that an event will be execute twice, but I + -- feel like that is a trade-off worth doing, since it otherwise refocus + -- would have to be more complicated (or at least, restructured). + node:input_enter() + end + session.current_nodes[vim.api.nvim_get_current_buf()] = node +end + -- make these lazy, such that we don't have to load them before it's really -- necessary (drives up cost of initial load, otherwise). -- stylua: ignore @@ -813,6 +855,7 @@ ls = util.lazy_table({ setup = require("luasnip.config").setup, extend_decorator = extend_decorator, log = require("luasnip.util.log"), + activate_node = activate_node }, ls_lazy) return ls diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index dbe141b5d..22de611b2 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -418,6 +418,10 @@ function ChoiceNode:subtree_set_rgrav(rgrav) end end +function ChoiceNode:extmarks_valid() + return node_util.generic_extmarks_valid(self, self.active_choice) +end + return { C = C, } diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 94f751c51..d97889772 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -423,6 +423,13 @@ function DynamicNode:subtree_set_rgrav(rgrav) end end +function DynamicNode:extmarks_valid() + if self.snip then + return node_util.generic_extmarks_valid(self, self.snip) + end + return true +end + return { D = D, } diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index 0039a371e..bd1cae1f4 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -95,24 +95,6 @@ function ExitNode:input_leave(no_move, dry_run) end end -function ExitNode:jump_into(dir, no_move, dry_run) - if not session.config.history then - self:input_enter(no_move, dry_run) - if (dir == 1 and not self.next) or (dir == -1 and not self.prev) then - if self.pos == 0 then - -- leave instantly, self won't be active snippet. - self:input_leave(no_move, dry_run) - end - return nil - else - return self - end - else - -- if no next node, return self as next current node. - return InsertNode.jump_into(self, dir, no_move, dry_run) or self - end -end - function ExitNode:_update_dependents() end function ExitNode:update_dependents() end function ExitNode:update_all_dependents() end @@ -180,16 +162,7 @@ function InsertNode:jump_into(dir, no_move, dry_run) if self:is_inner_active(dry_run) then if dir == 1 then if self.next then - if not dry_run then - self.inner_active = false - if not session.config.history then - self.inner_first = nil - self.inner_last = nil - end - else - dry_run.active[self] = false - end - + self:input_leave_children(dry_run) self:input_leave(no_move, dry_run) return self.next:jump_into(dir, no_move, dry_run) else @@ -197,16 +170,7 @@ function InsertNode:jump_into(dir, no_move, dry_run) end else if self.prev then - if not dry_run then - self.inner_active = false - if not session.config.history then - self.inner_first = nil - self.inner_last = nil - end - else - dry_run.active[self] = false - end - + self:input_leave_children(dry_run) self:input_leave(no_move, dry_run) return self.prev:jump_into(dir, no_move, dry_run) else @@ -219,48 +183,71 @@ function InsertNode:jump_into(dir, no_move, dry_run) end end -function InsertNode:jump_from(dir, no_move, dry_run) +function ExitNode:jump_from(dir, no_move, dry_run) self:init_dry_run_inner_active(dry_run) - if dir == 1 then - if self.inner_first then - if not dry_run then - self.inner_active = true - else - dry_run.active[self] = true - end + local next_node = util.ternary(dir == 1, self.next, self.prev) + local next_inner_node = util.ternary(dir == 1, self.inner_first, self.inner_last) - return self.inner_first:jump_into(dir, no_move, dry_run) - else - if self.next then - self:input_leave(no_move, dry_run) - return self.next:jump_into(dir, no_move, dry_run) - else - -- only happens for exitNodes, but easier to include here - -- and reuse this impl for them. - return self - end - end + if next_inner_node then + self:input_enter_children(dry_run) + return next_inner_node:jump_into(dir, no_move, dry_run) else - if self.inner_last then - if not dry_run then - self.inner_active = true - else - dry_run.active[self] = true + if next_node then + local next_node_dry_run = { active = {} } + -- don't have to `init_dry_run_inner_active` since this node does + -- not have children active if jump_from is called. + + -- true: don't move + local target_node = next_node:jump_into(dir, true, next_node_dry_run) + -- if there is no node that can serve as jump-target, just remain + -- here. + -- Regular insertNodes don't have to handle this, since there is + -- always an exitNode or another insertNode at their endpoints. + if not target_node then + return self end - return self.inner_last:jump_into(dir, no_move, dry_run) + self:input_leave(no_move, dry_run) + return next_node:jump_into(dir, no_move, dry_run) or self else - if self.prev then - self:input_leave(no_move, dry_run) - return self.prev:jump_into(dir, no_move, dry_run) - else - return self - end + return self end end end +function InsertNode:jump_from(dir, no_move, dry_run) + self:init_dry_run_inner_active(dry_run) + + local next_node = util.ternary(dir == 1, self.next, self.prev) + local next_inner_node = util.ternary(dir == 1, self.inner_first, self.inner_last) + + if next_inner_node then + self:input_enter_children(dry_run) + return next_inner_node:jump_into(dir, no_move, dry_run) + else + if next_node then + self:input_leave(no_move, dry_run) + return next_node:jump_into(dir, no_move, dry_run) + end + end +end + +function InsertNode:input_enter_children(dry_run) + if dry_run then + dry_run.active[self] = true + else + self.inner_active = true + end +end +function InsertNode:input_leave_children(dry_run) + if dry_run then + dry_run.active[self] = false + else + self.inner_active = false + end +end + function InsertNode:input_leave(_, dry_run) if dry_run then return diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index c73eb83a7..85e1f981a 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -178,6 +178,8 @@ function Node:input_leave(_, dry_run) self.mark:update_opts(self:get_passive_ext_opts()) end +function Node:input_leave_children() end +function Node:input_enter_children() end local function find_dependents(self, position_self, dict) local nodes = {} @@ -501,15 +503,15 @@ end -- -- Unfortunately, we cannot guarantee that two extmarks on the same position -- also have the same gravities, for exmample if the text inside a focused node --- is deleted, and then another unrelated node is focused, the two endpoints --- will have opposing rgravs. +-- is deleted, and then another unrelated node is focused, the two endpoints of +-- the previously focused node will have opposing rgravs. -- Maybe this whole procedure could be sped up further if we can assume that -- identical endpoints imply identical rgravs. local function focus_node(self, lrgrav, rrgrav) local abs_pos = vim.deepcopy(self.absolute_position) -- find nodes on path from self to root. - local nodes_path = node_util.get_nodes_between(self.parent.snippet, abs_pos) + local nodes_path = node_util.get_nodes_between(self.parent.snippet, self) -- nodes_on_path_to_self does not include the outer snippet, insert it here -- (and also insert some dummy-value in abs_pos, such that abs_pos[i] the -- position of node_path[i] in node_path[i-1] is). @@ -603,6 +605,24 @@ function Node:set_text(text) end end +-- since parents validate the adjacency, nodes where we don't know anything +-- about the text inside them just have to assume they haven't been deleted :D +function Node:extmarks_valid() + return true +end + +function Node:linkable() + -- linkable if insert or exitNode. + return vim.tbl_contains({types.insertNode, types.exitNode}, rawget(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 +end +function Node:leaf() + return vim.tbl_contains({types.textNode, types.functionNode, types.insertNode, types.exitNode}, rawget(self, "type")) +end + return { Node = Node, focus_node = focus_node, diff --git a/lua/luasnip/nodes/restoreNode.lua b/lua/luasnip/nodes/restoreNode.lua index b58f2ab00..a87cdc2b9 100644 --- a/lua/luasnip/nodes/restoreNode.lua +++ b/lua/luasnip/nodes/restoreNode.lua @@ -7,6 +7,7 @@ local RestoreNode = Node:new() local types = require("luasnip.util.types") local events = require("luasnip.util.events") local util = require("luasnip.util.util") +local node_util = require("luasnip.nodes.util") local mark = require("luasnip.util.mark").mark local extend_decorator = require("luasnip.util.extend_decorator") @@ -292,6 +293,10 @@ function RestoreNode:subtree_set_rgrav(rgrav) end end +function RestoreNode:extmarks_valid() + return node_util.generic_extmarks_valid(self, self.snip) +end + return { R = R, } diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 44393dd7e..53eb78f14 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -347,6 +347,10 @@ local function _S(snip, nodes, opts) -- * `update_dependents` can be called to find all dependents, and -- update the visible ones. dependents_dict = dict.new(), + + -- list of snippets expanded within the region of this snippet. + -- sorted by their buffer-position, for quick searching. + child_snippets = {} }), opts ) @@ -462,94 +466,178 @@ end extend_decorator.register(ISN, { arg_indx = 4 }) function Snippet:remove_from_jumplist() + if not self.visible then + -- snippet not visible => already removed. + -- Don't remove it twice. + return + end + -- prev is i(-1)(startNode), prev of that is the outer/previous snippet. + -- pre is $0 or insertNode. local pre = self.prev.prev -- similar for next, self.next is the i(0). + -- nxt is snippet. local nxt = self.next.next self:exit() - -- basically four possibilities: only snippet, between two snippets, - -- inside an insertNode (start), inside an insertNode (end). + local sibling_list = self.parent_node ~= nil and self.parent_node.parent.snippet.child_snippets or session.snippet_roots[vim.api.nvim_get_current_buf()] + local self_indx + for i, snip in ipairs(sibling_list) do + if snip == self then + self_indx = i + end + end + table.remove(sibling_list, self_indx) + + -- previous snippet jumps to this one => redirect to jump to next one. if pre then - -- Snippet is linearly behind previous snip, the appropriate value - -- for nxt.prev is set later. - if pre.pos == 0 then - pre.next = nxt - else - if nxt ~= pre then - -- if not the only snippet inside the insertNode: - pre.inner_first = nxt - nxt.prev = pre - return - else + if pre.inner_first == self then + if pre == nxt then pre.inner_first = nil - pre.inner_last = nil - pre.inner_active = false - return + else + pre.inner_first = nxt end + elseif pre.next == self then + pre.next = nxt end end if nxt then - if nxt.pos == -1 then - nxt.prev = pre - else - -- only possible if this is the last inside an insertNode, only - -- snippet in insertNode is handled above - nxt.inner_last = pre - pre.next = nxt + if nxt.inner_last == self.next then + if pre == nxt then + nxt.inner_last = nil + else + nxt.inner_last = pre + end + -- careful here!! nxt.prev is its start_node, nxt.prev.prev is this + -- snippet. + elseif nxt.prev.prev == self.next then + nxt.prev.prev = pre end end end -local function insert_into_jumplist(snippet, start_node, current_node) - if current_node then - -- currently at the endpoint (i(0)) of another snippet, this snippet - -- is inserted _behind_ that snippet. - if current_node.pos == 0 then - if current_node.next then - if current_node.next.pos == -1 then - -- next is beginning of another snippet, this snippet is - -- inserted before that one. - current_node.next.prev = snippet.insert_nodes[0] - else - -- next is outer insertNode. - current_node.next.inner_last = snippet.insert_nodes[0] +local function insert_into_jumplist(snippet, start_node, current_node, parent_node, sibling_snippets, own_indx) + local prev_snippet = sibling_snippets[own_indx-1] + -- have not yet inserted self!! + local next_snippet = sibling_snippets[own_indx] + + -- 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 + -- because they have the same parent_node. + local prev, next + if prev_snippet ~= nil and prev_snippet.parent_node == parent_node then + prev = prev_snippet + end + if next_snippet ~= nil and next_snippet.parent_node == parent_node then + next = next_snippet + end + + -- whether roots should be linked together. + local link_roots = session.config.link_roots + + -- whether children of the same snippet should be linked to their parent + -- and eachother. + local link_children = session.config.link_children + + if parent_node then + if node_util.linkable_node(parent_node) then + -- snippetNode (which has to be empty to be viable here) and + -- insertNode can both deal with inserting a snippet inside them + -- (ie. hooking it up st. it can be visited after jumping back to + -- the snippet of parent). + -- 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 + 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 end + -- make sure we can jump back to the parent. + start_node.prev = parent_node end - snippet.insert_nodes[0].next = current_node.next - current_node.next = start_node - start_node.prev = current_node - elseif current_node.pos == -1 then - if current_node.prev then - if current_node.prev.pos == 0 then - current_node.prev.next = start_node - else - current_node.prev.inner_first = snippet + + -- 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] + -- jump from $0 to next snippet (skip its start_node) + snippet.insert_nodes[0].next = next + else + if link_children then + parent_node.inner_last = snippet.insert_nodes[0] end + snippet.insert_nodes[0].next = parent_node end - snippet.insert_nodes[0].next = current_node - start_node.prev = current_node.prev - current_node.prev = snippet.insert_nodes[0] else - snippet.insert_nodes[0].next = current_node - -- jump into snippet directly. - current_node.inner_first = snippet - current_node.inner_last = snippet.insert_nodes[0] + -- naively, even if the parent is linkable, there might be snippets + -- before/after that share the same parent, so we could + -- theoretically link up with them. + -- This, however, can cause cyclic jumps, for example if the + -- previous child-snippet contains the current node: we will jump + -- from the end of the new snippet into the previous child-snippet, + -- and from its last node into the new snippet. + -- Since cycles should be avoided (very weird if the jumps just go + -- in a circle), we have no choice but to fall back to this + -- old-style linkage. + + -- Don't jump from current_node to this snippet (I feel + -- like that should be good: one can still get back to ones + -- 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 + 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 + 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] end end - -- snippet is between i(-1)(startNode) and i(0). - snippet.next = snippet.insert_nodes[0] - snippet.prev = start_node - - snippet.insert_nodes[0].prev = snippet - start_node.next = snippet + table.insert(sibling_snippets, own_indx, snippet) end function Snippet:trigger_expand(current_node, pos_id, env) local pos = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, pos_id, {}) + + -- find tree-node the snippet should be inserted at (could be before another node). + local _, sibling_snippets, own_indx, parent_node = node_util.snippettree_find_undamaged_node(pos, { + tree_respect_rgravs = false, + tree_preference = node_util.binarysearch_preference.outside, + snippet_mode = "linkable" + }) + + if current_node then + if parent_node then + if node_util.linkable_node(parent_node) then + node_util.refocus(current_node, parent_node) + parent_node:input_enter_children() + else + -- enter extmarks of parent_node, but don't enter it + -- "logically", it will not be the parent of the snippet. + parent_node:focus() + -- 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) + end + end + local pre_expand_res = self:event(events.pre_expand, { expand_pos = pos }) or {} -- update pos, event-callback might have moved the extmark. @@ -576,9 +664,9 @@ function Snippet:trigger_expand(current_node, pos_id, env) local parent_ext_base_prio -- if inside another snippet, increase priority accordingly. - -- for now do a check for .indx. - if current_node and (current_node.indx and current_node.indx > 1) then - parent_ext_base_prio = current_node.parent.ext_opts.base_prio + -- parent_node is only set if this snippet is expanded inside another one. + if parent_node then + parent_ext_base_prio = parent_node.parent.ext_opts.base_prio else parent_ext_base_prio = session.config.ext_base_prio end @@ -623,9 +711,26 @@ function Snippet:trigger_expand(current_node, pos_id, env) -- 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} start_node.parent = self - insert_into_jumplist(self, start_node, current_node) + -- hook up i0 and start_node, and then the snippet itself. + -- they are outside, not inside the snippet. + -- This should clearly be the case for start_node, but also for $0 since + -- jumping to $0 should make/mark the snippet non-active (for example via + -- extmarks) + start_node.next = self + self.prev = start_node + 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 + + insert_into_jumplist(self, start_node, current_node, parent_node, sibling_snippets, own_indx) + + return parent_node end -- returns copy of snip if it matches, nil if not. @@ -976,6 +1081,15 @@ end -- used in LSP-Placeholders. 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() + end + end + self.visible = false for _, node in ipairs(self.nodes) do node:exit() @@ -1127,6 +1241,11 @@ function Snippet:update_all_dependents_static() end function Snippet:resolve_position(position) + -- only snippets have -1-node. + if position == -1 and self.type == types.snippet then + return self.prev + end + return self.nodes[position] end @@ -1242,6 +1361,142 @@ function Snippet:subtree_set_rgrav(rgrav) end end +-- for this to always return a node if pos is withing the snippet-boundaries, +-- the snippet must have valid extmarks. +-- Looks for a node that has a specific property (either linkable, or +-- interactive), which can be indicated by setting mode to either of the two +-- (as string). +function Snippet:node_at(pos, mode) + if #self.nodes == 0 then + -- special case: no children (can naturally occur with dynamicNode, + -- when its function could not be evaluated, or if a user passed an empty snippetNode). + return self + end + + -- collect nodes where pos is either in gravity-adjusted boundaries, .. + local gravity_matches = {} + -- .. or just inside the regular boundaries. + -- Both are needed, so we can fall back to matches if there is no gravity_match + -- with the desired mode ("linkable" or "interactive"), fall back to + -- extmark_matches if there is also no regular match with the desired mode, + -- and finally fall back to any match (still preferring extmark-match) if + -- there is no match with the desired mode at all. + -- Unfortunately, all this is necessary, since there are many cases where + -- we may return no linkable node, despite there apparently being one in + -- reach of the cursor. + local matches = {} + -- find_node visits all nodes in-order until the predicate returns true. + self:find_node(function(node) + if not node:leaf() then + -- not a leaf-node. + return false + end + + local node_mark = node.mark + local node_from, node_to = node_mark:pos_begin_end_raw() + -- if pos certainly beyond node, quickly continue. + -- This means a little more work for the nodes in range of pos, while + -- all nodes well before it are quickly skipped => should benefit + -- all cases where the runtime of this is noticeable, and which are not + -- unrealistic (lots of zero-width nodes). + if util.pos_cmp(pos, {node_to[1], node_to[2]+1}) > 0 then + return false + end + + -- generate gravity-adjusted endpoints. + local grav_adjusted_from = {node_from[1], node_from[2]} + local grav_adjusted_to = {node_to[1], node_to[2]} + if node_mark:get_rgrav(-1) then + grav_adjusted_from[2] = grav_adjusted_from[2] + 1 + end + if node_mark:get_rgrav(1) then + grav_adjusted_to[2] = grav_adjusted_to[2] + 1 + end + + local cmp_pos_to = util.pos_cmp(pos, node_to) + local cmp_pos_from = util.pos_cmp(pos, node_from) + local cmp_grav_from = util.pos_cmp(pos, grav_adjusted_from) + local cmp_grav_to = util.pos_cmp(pos, grav_adjusted_to) + + if cmp_pos_from < 0 then + -- abort once the first node is definitely beyond pos. + -- (extmark-gravity can't move column to the left). + return true + end + + -- pos between from,to <=> from <= pos < to is used when choosing which + -- extmark to insert text into, so we should adopt it here. + if cmp_grav_from >= 0 and cmp_grav_to < 0 then + table.insert(gravity_matches, node) + end + -- matches does not have to respect the extmark-conventions, just catch + -- all possible nodes. + if cmp_pos_from >= 0 and cmp_pos_to <= 0 then + table.insert(matches, node) + end + end) + + -- instead of stupid nesting ifs, and because we can't use goto since + -- non-luajit-users should also be able to run luasnip :((( + return (function() + for _, node in ipairs(gravity_matches) do + if node[mode](node) then + return node + end + end + for _, node in ipairs(matches) do + if node[mode](node) then + return node + end + end + -- no interactive node found, fall back to any match. + return gravity_matches[1] or matches[1] + end)() +end + +-- return the node the snippet jumps to, or nil if there isn't one. +function Snippet:next_node() + -- self.next is $0, .next is either the surrounding node, or the next + -- snippet in the list, .prev is the i(-1) if the self.next.next is the + -- next snippet. + + if self.parent_node and self.next.next == self.parent_node then + return self.next.next + else + return (self.next.next and self.next.next.prev) + end +end + +function Snippet:extmarks_valid() + -- assumption: extmarks are contiguous, and all can be queried via pos_begin_end_raw. + local ok, current_from, self_to = pcall(self.mark.pos_begin_end_raw, self.mark) + if not ok then + return false + end + + -- below code does not work correctly if the snippet(Node) does not have any children. + if #self.nodes == 0 then + return true + end + + for _, node in ipairs(self.nodes) do + local ok_, node_from, node_to = 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 + -- - any child of this node violates these rules. + if not ok_ or util.pos_cmp(current_from, node_from) ~= 0 or not node:extmarks_valid() then + return false + end + current_from = node_to + end + if util.pos_cmp(current_from, self_to) ~= 0 then + return false + end + + return true +end + return { Snippet = Snippet, S = S, diff --git a/lua/luasnip/nodes/textNode.lua b/lua/luasnip/nodes/textNode.lua index 492cf2197..6994f9f7d 100644 --- a/lua/luasnip/nodes/textNode.lua +++ b/lua/luasnip/nodes/textNode.lua @@ -47,6 +47,18 @@ function TextNode:is_interactive() return false end +function TextNode:extmarks_valid() + local from, to = self.mark:pos_begin_end_raw() + if util.pos_cmp(from, to) == 0 and not (#self.static_text == 0 or (#self.static_text == 1 and #self.static_text[1] == 0)) then + -- assume the snippet is invalid if a textNode occupies zero space, + -- but has text which would occupy some. + -- This should allow some modifications, but as soon as a textNode is + -- deleted entirely, we sound the alarm :D + return false + end + return true +end + return { T = T, textNode = TextNode, diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index df33bc82d..81e752f75 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -2,6 +2,7 @@ local util = require("luasnip.util.util") local ext_util = require("luasnip.util.ext_opts") local types = require("luasnip.util.types") local key_indexer = require("luasnip.nodes.key_indexer") +local session = require("luasnip.session") local function subsnip_init_children(parent, children) for _, child in ipairs(children) do @@ -62,9 +63,19 @@ local function wrap_args(args) end end -local function get_nodes_between(parent, child_pos) +local function get_nodes_between(parent, child) local nodes = {} + -- special case for nodes without absolute_position (which is only + -- start_node). + if child.pos == -1 then + -- no nodes between, only child. + nodes[1] = child + return nodes + end + + local child_pos = child.absolute_position + local indx = #parent.absolute_position + 1 local prev = parent while child_pos[indx] do @@ -77,19 +88,38 @@ local function get_nodes_between(parent, child_pos) return nodes end +-- assumes that children of child are not even active. +-- If they should also be left, do that separately. +-- Does not leave the parent. local function leave_nodes_between(parent, child, no_move) - local nodes = get_nodes_between(parent, child.absolute_position) + local nodes = get_nodes_between(parent, child) + if #nodes == 0 then + return + end + -- reverse order, leave child first. - for i = #nodes, 1, -1 do + for i = #nodes, 2, -1 do + -- this only happens for nodes where the parent will also be left + -- entirely (because we stop at nodes[2], and handle nodes[1] + -- separately) nodes[i]:input_leave(no_move) + nodes[i-1]:input_leave_children() end + nodes[1]:input_leave(no_move) end local function enter_nodes_between(parent, child, no_move) - local nodes = get_nodes_between(parent, child.absolute_position) - for _, node in ipairs(nodes) do - node:input_enter(no_move) + local nodes = get_nodes_between(parent, child) + if #nodes == 0 then + return + end + + for i = 1, #nodes-1 do + -- only enter children for nodes before the last (lowest) one. + nodes[i]:input_enter(no_move) + nodes[i]:input_enter_children() end + nodes[#nodes]:input_enter(no_move) end local function select_node(node) @@ -147,6 +177,461 @@ local function wrap_context(context) end end +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")) +end + +-- mainly used internally, by binarysearch_pos. +-- these are the nodes that are definitely not linkable, there are nodes like +-- dynamicNode or snippetNode that might be linkable, depending on their +-- content. Could look into that to make this more complete, but that does not +-- 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}, rawget(node, "type")) +end +-- return whether a node is certainly (not) interactive. +-- Coincindentially, the same nodes as (non-)linkable ones, but since there is a +-- semantic difference, use separate names. +local interactive_node = linkable_node +local non_interactive_node = non_linkable_node + +local function prefer_nodes(prefer_func, reject_func) + return function(cmp_mid_to, cmp_mid_from, mid_node) + local reject_mid = reject_func(mid_node) + local prefer_mid = prefer_func(mid_node) + + -- if we can choose which node to continue in, prefer the one that + -- may be linkable/interactive. + if cmp_mid_to == 0 and reject_mid then + return true, false + elseif cmp_mid_from == 0 and reject_mid then + return false, true + elseif (cmp_mid_to == 0 or cmp_mid_from == 0) and prefer_mid then + return false, false + else + return cmp_mid_to >= 0, cmp_mid_from < 0 + end + end +end + +-- functions for resolving conflicts, if `pos` is on the boundary of two nodes. +-- Return whether to continue behind or before mid (in that order). +-- At most one of those may be true, of course. +local binarysearch_preference = { + outside = function(cmp_mid_to, cmp_mid_from, _) + return cmp_mid_to >= 0, cmp_mid_from <= 0 + end, + inside = function(cmp_mid_to, cmp_mid_from, _) + return cmp_mid_to > 0, cmp_mid_from < 0 + end, + linkable = prefer_nodes(linkable_node, non_linkable_node), + interactive = prefer_nodes(interactive_node, non_interactive_node) +} +-- `nodes` is a list of nodes ordered by their occurrence in the buffer. +-- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT +-- EDGE(/side) of `pos` is inside. +-- This convention is chosen since a snippet inserted at `pos` will move the +-- character at `pos` to the right. +-- The exact meaning of "inside" can be influenced with `respect_rgravs` and +-- `boundary_resolve_mode`: +-- * if `respect_rgravs` is true, "inside" emulates the shifting-behaviour of +-- extmarks: +-- First of all, we compare the left edge of `pos` with the left/right edges +-- of from/to, depending on rgrav. +-- If the left edge is <= left/right edge of from, and < left/right edge of +-- to, `pos` is inside the node. +-- +-- * if `respect_rgravs` is false, pos has to be fully inside a node to be +-- considered inside it. If pos is on the left endpoint, it is considered to be +-- left of the node, and likewise for the right endpoint. +-- +-- * `boundary_resolve_mode` changes how a position on the boundary of a node +-- is treated: +-- * for `"prefer_linkable/interactive"`, we assume that the nodes in `nodes` are +-- contiguous, and prefer falling into the previous/next node if `pos` is on +-- mid's boundary, and mid is not linkable/interactie. +-- This way, we are more likely to return a node that can handle a new +-- snippet/is interactive. +-- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd +-- like to find a position between two nodes. +-- This mode makes sense for finding the snippet a new snippet should be +-- inserted in, since we'd like to prefer inserting before/after a snippet, if +-- the position is ambiguous. +-- +-- In general: +-- These options are useful for making this function more general: When +-- searching in the contiguous nodes of a snippet, we'd like this routine to +-- return any of them (obviously the one pos is inside/or on the border of, and +-- we'd like to prefer returning a node that can be linked), but in no case +-- fail. +-- However! when searching the top-level snippets with the intention of finding +-- the snippet/node a new snippet should be expanded inside, it seems better to +-- shift an existing snippet to the right/left than expand the new snippet +-- inside it (when the expand-point is on the boundary). +local function binarysearch_pos(nodes, pos, respect_rgravs, boundary_resolve_mode) + local left = 1 + local right = #nodes + + -- actual search-routine from + -- https://github.com/Roblox/Wiki-Lua-Libraries/blob/master/StandardLibraries/BinarySearch.lua + if #nodes == 0 then + return nil, 1 + end + while true do + local mid = left + math.floor((right-left)/2) + local mid_mark = nodes[mid].mark + local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark) + + if not ok then + -- error while running this procedure! + -- return false (because I don't know how to do this with `error` + -- and the offending node). + -- (returning data instead of a message in `error` seems weird..) + return false, mid + end + + if respect_rgravs then + -- if rgrav is set on either endpoint, the node considers its + -- endpoint to be the right, not the left edge. + -- We only want to work with left edges but since the right edge is + -- the left edge of the next column, this is not an issue :) + -- TODO: does this fail with multibyte characters??? + if mid_mark:get_rgrav(-1) then + mid_from[2] = mid_from[2] + 1 + end + if mid_mark:get_rgrav(1) then + mid_to[2] = mid_to[2] + 1 + end + end + + local cmp_mid_to = util.pos_cmp(pos, mid_to) + local cmp_mid_from = util.pos_cmp(pos, mid_from) + + local cont_behind_mid, cont_before_mid = boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid]) + + if cont_behind_mid then + -- make sure right-left becomes smaller. + left = mid + 1 + if left > right then + return nil, mid + 1 + end + elseif cont_before_mid then + -- continue search on left side + right = mid - 1 + if left > right then + return nil, mid + end + else + -- greater-equal than mid_from, smaller or equal to mid_to => left edge + -- of pos is inside nodes[mid] :) + return nodes[mid], mid + end + end +end + +-- a and b have to be in the same snippet, return their first (as seen from +-- them) common parent. +local function first_common_node(a, b) + local a_pos = a.absolute_position + local b_pos = b.absolute_position + + -- last as seen from root. + local i = 0 + local last_common = a.parent.snippet + -- invariant: last_common is parent of both a and b. + while (a_pos[i+1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do + last_common = last_common:resolve_position(a_pos[i + 1]) + i = i + 1 + end + + return last_common +end + +-- roots at depth 0, children of root at depth 1, their children at 2, ... +local function snippettree_depth(snippet) + local depth = 0 + while snippet.parent_node ~= nil do + snippet = snippet.parent_node.parent.snippet + depth = depth + 1 + end + return depth +end + +-- find the first common snippet a and b have on their respective unique paths +-- to the snippet-roots. +-- if no common ancestor exists (ie. a and b are roots of their buffers' +-- forest, or just in different trees), return nil. +-- in both cases, the paths themselves are returned as well. +-- The common ancestor is included in the paths, except if there is none. +-- Instead of storing the snippets in the paths, they are represented by the +-- node which contains the next-lower snippet in the path (or `from`/`to`, if it's +-- the first node of the path) +-- This is a bit complicated, but this representation contains more information +-- (or, more easily accessible information) than storing snippets: the +-- immediate parent of the child along the path cannot be easily retrieved if +-- the snippet is stored, but the snippet can be easily retrieved if the child +-- is stored (.parent.snippet). +-- And, so far this is pretty specific to refocus, and thus modeled so there is +-- very little additional work in that method. +-- At most one of a,b may be nil. +local function first_common_snippet_ancestor_path(a, b) + local a_path = {} + local b_path = {} + + -- general idea: we find the depth of a and b, walk upward with the deeper + -- one until we find its first ancestor with the same depth as the less + -- deep snippet, and then follow both paths until they arrive at the same + -- snippet (or at the root of their respective trees). + -- if either is nil, we treat it like it's one of the roots (the code will + -- behave correctly this way, and return an empty path for the nil-node, + -- and the correct path for the non-nil one). + local a_depth = a ~= nil and snippettree_depth(a) or 0 + local b_depth = b ~= nil and snippettree_depth(b) or 0 + + -- bit subtle: both could be 0, but one could be nil. + -- deeper should not be nil! (this allows us to do the whole walk for the + -- non-nil node in the first for-loop, as opposed to needing some special + -- handling). + local deeper, deeper_path, other, other_path + if b == nil or (a ~= nil and a_depth > b_depth) then + deeper = a + other = b + deeper_path = a_path + other_path = b_path + else + -- we land here if `b ~= nil and (a == nil or a_depth >= b_depth)`, so + -- exactly what we want. + deeper = b + other = a + deeper_path = b_path + other_path = a_path + end + + for _ = 1, math.abs(a_depth - b_depth) do + table.insert(deeper_path, deeper.parent_node) + deeper = deeper.parent_node.parent.snippet + end + -- here: deeper and other are at the same depth. + -- If we walk upwards one step at a time, they will meet at the same + -- parent, or hit their respective roots. + + -- deeper can't be nil, if other is, we are done here and can return the + -- paths (and there is no shared node) + if other == nil then + return nil, a_path, b_path + end + -- beyond here, deeper and other are not nil. + + while deeper ~= other do + if deeper.parent_node == nil then + -- deeper is at depth 0 => other as well => both are roots. + return nil, a_path, b_path + end + + table.insert(deeper_path, deeper.parent_node) + table.insert(other_path, other.parent_node) + + -- walk one step towards root. + deeper = deeper.parent_node.parent.snippet + other = other.parent_node.parent.snippet + end + + -- either one will do here. + return deeper, a_path, b_path +end + +-- removes focus from `from` and upwards up to the first common ancestor +-- (node!) of `from` and `to`, and then focuses nodes between that f.c.a. and +-- `to`. +-- Requires that `from` is currently entered/focused, and that no snippet +-- between `to` and its root is invalid. +local function refocus(from, to) + if from == nil and to == nil then + -- absolutely nothing to do, should not happen. + return + end + -- pass nil if from/to is nil. + -- if either is nil, first_common_node is nil, and the corresponding list empty. + local first_common_snippet, from_snip_path, to_snip_path = first_common_snippet_ancestor_path(from and from.parent.snippet, to and to.parent.snippet) + + -- we want leave/enter_path to be s.t. leaving/entering all nodes between + -- each entry and its snippet (and the snippet itself) will leave/enter all + -- nodes between the first common snippet (or the root-snippet) and + -- from/to. + -- Then, the nodes between the first common node and the respective + -- entrypoints (also nodes) into the first common snippet will have to be + -- left/entered, which is handled by final_leave_/first_enter_/common_node. + + -- from, to are not yet in the paths. + table.insert(from_snip_path, 1, from) + table.insert(to_snip_path, 1, to) + + -- determine how far to leave: if there is a common snippet, only up to the + -- first (from from/to) common node, otherwise leave the one snippet, and + -- enter the other completely. + local final_leave_node, first_enter_node, common_node + if first_common_snippet then + -- there is a common snippet => there is a common node => we have to + -- set final_leave_node, first_enter_node, and common_node. + final_leave_node = from_snip_path[#from_snip_path] + first_enter_node = to_snip_path[#to_snip_path] + common_node = first_common_node(first_enter_node, final_leave_node) + + -- Also remove these last nodes from the lists, their snippet is not + -- supposed to be left entirely. + from_snip_path[#from_snip_path] = nil + to_snip_path[#to_snip_path] = nil + end + + -- now do leave/enter, set no_move on all operations. + -- if one of from/to was nil, there are no leave/enter-operations done for + -- it (from/to_snip_path is {}, final_leave/first_enter_* is nil). + + -- leave_children on all from-nodes except the original from. + if #from_snip_path > 0 then + -- we know that the first node is from. + local ok1 = pcall(leave_nodes_between, from.parent.snippet, from, true) + -- leave_nodes_between does not affect snippet, so that has to be left + -- here. + -- snippet does not have input_leave_children, so only input_leave + -- needs to be called. + local ok2 = pcall(from.parent.snippet.input_leave, from.parent.snippet, true) + if not ok1 or not ok2 then + from.parent.snippet:remove_from_jumplist() + end + end + for i = 2, #from_snip_path do + local node = from_snip_path[i] + local ok1 = pcall(node.input_leave_children, node) + local ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true) + local ok3 = pcall(node.parent.snippet.input_leave, node.parent.snippet, true) + if not ok1 or not ok2 or not ok3 then + from.parent.snippet:remove_from_jumplist() + end + end + + -- this leave, and the following enters should be safe: the path to `to` + -- was verified via extmarks_valid (precondition). + if common_node and final_leave_node then + -- if the final_leave_node is from, its children are not active (which + -- stems from the requirement that from is the currently active node), + -- and so don't have to be left. + if final_leave_node ~= from then + final_leave_node:input_leave_children() + end + leave_nodes_between(common_node, final_leave_node, true) + end + + if common_node and first_enter_node then + -- In general we assume that common_node is active when we are here. + -- This may not be the case if we are currently inside the i(0) or + -- i(-1), since the snippet might be the common node and in this case, + -- it is inactive. + -- This means that, if we want to enter a non-exitNode, we have to + -- explicitly activate the snippet for all jumps to behave correctly. + -- (if we enter a i(0)/i(-1), this is not necessary, of course). + if final_leave_node.type == types.exitNode and first_enter_node.type ~= types.exitNode then + common_node:input_enter(true) + end + -- symmetrically, entering an i(0)/i(-1) requires leaving the snippet. + if final_leave_node.type ~= types.exitNode and first_enter_node.type == types.exitNode then + common_node:input_leave(true) + end + + enter_nodes_between(common_node, first_enter_node, true) + + -- if the `first_enter_node` is already `to` (occurs if `to` is in the + -- common snippet of to and from), we should not enter its children. + -- (we only want to `input_enter` to.) + if first_enter_node ~= to then + first_enter_node:input_enter_children() + end + end + + -- same here, input_enter_children has to be called manually for the + -- to-nodes of the path we are entering (since enter_nodes_between does not + -- call it for the child-node). + + for i = #to_snip_path, 2, -1 do + local node = to_snip_path[i] + if node.type ~= types.exitNode then + node.parent.snippet:input_enter(true) + else + to.parent.snippet:input_leave(true) + end + enter_nodes_between(node.parent.snippet, node, true) + node:input_enter_children() + end + if #to_snip_path > 0 then + if to.type ~= types.exitNode then + to.parent.snippet:input_enter(true) + else + to.parent.snippet:input_leave(true) + end + enter_nodes_between(to.parent.snippet, to, true) + end +end + +local function generic_extmarks_valid(node, child) + -- valid if + -- - extmark-extents match. + -- - current choice is valid + local ok1, self_from, self_to = pcall(node.mark.pos_begin_end_raw, node.mark) + local ok2, child_from, child_to = pcall(child.mark.pos_begin_end_raw, child.mark) + + if not ok1 or not ok2 or util.pos_cmp(self_from, child_from) ~= 0 or util.pos_cmp(self_to, child_to) ~= 0 then + return false + end + return child:extmarks_valid() +end + +-- returns: * the smallest known snippet `pos` is inside. +-- * the list of other snippets inside the snippet of this smallest +-- node +-- * the index this snippet would be at if inserted into that list +-- * the node of this snippet pos is on. +local function snippettree_find_undamaged_node(pos, opts) + local prev_parent, child_indx, found_parent + local prev_parent_children = session.snippet_roots[vim.api.nvim_get_current_buf()] + + while true do + -- false: don't respect rgravs. + -- Prefer inserting the snippet outside an existing one. + found_parent, child_indx = binarysearch_pos(prev_parent_children, pos, opts.tree_respect_rgravs, opts.tree_preference) + if found_parent == false then + -- if the procedure returns false, there was an error getting the + -- position of a node (in this case, that node is a snippet). + -- 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() + 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 + -- input_enter all the nodes along the insertion-path correctly). + found_parent:remove_from_jumplist() + -- continue again with same parent, but one less snippet in its + -- children => shouldn't cause endless loop. + elseif found_parent == nil then + break + else + prev_parent = found_parent + -- can index prev_parent, since found_parent is not nil, and + -- assigned to prev_parent. + prev_parent_children = prev_parent.child_snippets + end + end + + local node + if prev_parent then + -- if found, find node to insert at, prefer receiving a linkable node. + node = prev_parent:node_at(pos, opts.snippet_mode) + end + + return prev_parent, prev_parent_children, child_indx, node +end + return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -160,4 +645,11 @@ return { print_dict = print_dict, init_node_opts = init_node_opts, snippet_extend_context = snippet_extend_context, + linkable_node = linkable_node, + binarysearch_pos = binarysearch_pos, + binarysearch_preference = binarysearch_preference, + refocus = refocus, + generic_extmarks_valid = generic_extmarks_valid, + snippettree_find_undamaged_node = snippettree_find_undamaged_node, + interactive_node = interactive_node } diff --git a/lua/luasnip/session/init.lua b/lua/luasnip/session/init.lua index d7afc03fd..77447c3ce 100644 --- a/lua/luasnip/session/init.lua +++ b/lua/luasnip/session/init.lua @@ -13,6 +13,16 @@ setmetatable(M.ft_redirect, { }) M.current_nodes = {} +-- roots of snippet-trees, per-buffer. +-- snippet_roots[n] => list of snippet-roots in buffer n. +M.snippet_roots = setmetatable({}, { + -- create missing lists automatically. + __index = function(t,k) + local new_t = {} + rawset(t, k, new_t) + return new_t + end +}) M.ns_id = vim.api.nvim_create_namespace("Luasnip") M.active_choice_nodes = {} diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 0b816ab1e..1d625e82d 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -612,6 +612,24 @@ local function ternary(cond, if_val, else_val) end end +-- just compare two integers. +local function cmp(i1, i2) + -- lets hope this ends up as one cmp. + if i1 < i2 then + return -1 + end + if i1 > i2 then + return 1 + end + return 0 +end + +-- compare two positions, <0 => pos1 pos1=pos2, >0 => pos1 > pos2. +local function pos_cmp(pos1, pos2) + -- if row is different it determines result, otherwise the column does. + return 2*cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2]) +end + return { get_cursor_0ind = get_cursor_0ind, set_cursor_0ind = set_cursor_0ind, @@ -659,4 +677,5 @@ return { lazy_table = lazy_table, ternary = ternary, jsregexp = jsregexp_ok and jsregexp, + pos_cmp = pos_cmp, } diff --git a/tests/helpers.lua b/tests/helpers.lua index 9d701ab47..3b89a3d24 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -36,6 +36,8 @@ function M.session_setup_luasnip(opts) else setup_parsers = false end + -- nil or true. + local hl_choiceNode = opts.hl_choiceNode -- stylua: ignore helpers.exec("set rtp+=" .. os.getenv("LUASNIP_SOURCE")) @@ -65,17 +67,29 @@ function M.session_setup_luasnip(opts) ]]) end - helpers.exec_lua( - [[ + helpers.exec_lua([[ + local hl_choiceNode, setup_extend = ... + -- MYVIMRC might not be set when nvim is loaded like this. vim.env.MYVIMRC = "/.vimrc" ls = require("luasnip") ls.setup(vim.tbl_extend("force", { store_selection_keys = "" - }, ...)) + }, hl_choiceNode and { + ext_opts = { + [require("luasnip.util.types").choiceNode] = { + active = { + virt_text = {{"●", "ErrorMsg"}}, + priority = 0 + }, + } + }, + } or {}, setup_extend)) ]], - setup_extend + -- passing nil here means the argument-list is terminated, I think. + -- Just pass false instead of nil/false. + hl_choiceNode or false, setup_extend ) if not no_snip_globals then diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua new file mode 100644 index 000000000..e4d44e621 --- /dev/null +++ b/tests/integration/session_spec.lua @@ -0,0 +1,1745 @@ +-- Test longer-running sessions of snippets. +-- Should cover things like deletion (handle removed text gracefully) and insertion. +local helpers = require("test.functional.helpers")(after_each) +local exec_lua, feed, exec = helpers.exec_lua, helpers.feed, helpers.exec +local ls_helpers = require("helpers") +local Screen = require("test.functional.ui.screen") + +local function expand() exec_lua("ls.expand()") end +local function jump(dir) exec_lua("ls.jump(...)", dir) end +local function change(dir) exec_lua("ls.change_choice(...)", dir) end + +describe("session", function() + local screen + + before_each(function() + helpers.clear() + ls_helpers.setup_jsregexp() + ls_helpers.session_setup_luasnip({hl_choiceNode = true}) + + -- add a rather complicated snippet. + -- It may be a bit hard to grasp, but will cover lots and lots of + -- edge-cases. + exec_lua([[ + local function jdocsnip(args, _, old_state) + local nodes = { + t({"/**"," * "}), + old_state and i(1, old_state.descr:get_text()) or i(1, {"A short Description"}), + t({"", ""}) + } + + -- These will be merged with the snippet; that way, should the snippet be updated, + -- some user input eg. text can be referred to in the new snippet. + local param_nodes = { + descr = nodes[2] + } + + -- At least one param. + if string.find(args[2][1], " ") then + vim.list_extend(nodes, {t({" * ", ""})}) + end + + local insert = 2 + for indx, arg in ipairs(vim.split(args[2][1], ", ", true)) do + -- Get actual name parameter. + arg = vim.split(arg, " ", true)[2] + if arg then + arg = arg:gsub(",", "") + local inode + -- if there was some text in this parameter, use it as static_text for this new snippet. + if old_state and old_state["arg"..arg] then + inode = i(insert, old_state["arg"..arg]:get_text()) + else + inode = i(insert) + end + vim.list_extend(nodes, {t({" * @param "..arg.." "}), inode, t({"", ""})}) + param_nodes["arg"..arg] = inode + + insert = insert + 1 + end + end + + if args[1][1] ~= "void" then + local inode + if old_state and old_state.ret then + inode = i(insert, old_state.ret:get_text()) + else + inode = i(insert) + end + + vim.list_extend(nodes, {t({" * ", " * @return "}), inode, t({"", ""})}) + param_nodes.ret = inode + insert = insert + 1 + end + + if vim.tbl_count(args[3]) ~= 1 then + local exc = string.gsub(args[3][2], " throws ", "") + local ins + if old_state and old_state.ex then + ins = i(insert, old_state.ex:get_text()) + else + ins = i(insert) + end + vim.list_extend(nodes, {t({" * ", " * @throws "..exc.." "}), ins, t({"", ""})}) + param_nodes.ex = ins + insert = insert + 1 + end + + vim.list_extend(nodes, {t({" */"})}) + + local snip = sn(nil, nodes) + -- Error on attempting overwrite. + snip.old_state = param_nodes + return snip + end + + ls.add_snippets("all", { + s({trig="fn"}, { + d(6, jdocsnip, {ai[2], ai[4], ai[5]}), t({"", ""}), + c(1, { + t({"public "}), + t({"private "}) + }), + c(2, { + t({"void"}), + i(nil, {""}), + t({"String"}), + t({"char"}), + t({"int"}), + t({"double"}), + t({"boolean"}), + }), + t({" "}), + i(3, {"myFunc"}), + t({"("}), i(4), t({")"}), + c(5, { + t({""}), + sn(nil, { + t({""," throws "}), + i(1) + }) + }), + t({" {", "\t"}), + i(0), + t({"", "}"}) + }) + }) + ]]) + + screen = Screen.new(50, 30) + 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 }, + [4] = {background = Screen.colors.Red1, foreground = Screen.colors.White} + }) + end) + + it("Deleted snippet is handled properly in expansion.", function() + feed("ofn") + exec_lua("ls.expand()") + 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:~ }| + {2:-- INSERT --} |]]} + jump(1) jump(1) jump(1) + screen:expect{grid=[[ + | + | + /** | + * A short Description | + */ | + public void myFunc(^) { | + | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- delete whole buffer. + feed("ggVGcfn") + -- immediately expand at the old position of the snippet. + exec_lua("ls.expand()") + -- first jump goes to i(-1), second might go back into deleted snippet, + -- if we did something wrong. + jump(-1) jump(-1) + screen:expect{grid=[[ + ^/** | + * A short Description | + */ | + public void myFunc() { | + | + } | + {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 --} |]]} + -- seven jumps to go to i(0), 8th, again, should not do anything. + jump(1) jump(1) jump(1) jump(1) + jump(1) jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + ^ | + } | + {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 --} |]]} + jump(1) + screen:expect{unchanged = true} + end) + it("Deleted snippet is handled properly when jumping.", function() + feed("ofn") + exec_lua("ls.expand()") + 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:~ }| + {2:-- INSERT --} |]]} + jump(1) jump(1) jump(1) + screen:expect{grid=[[ + | + | + /** | + * A short Description | + */ | + public void myFunc(^) { | + | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- delete whole buffer. + feed("ggVGd") + -- should not cause an error. + jump(1) + end) + it("Deleting nested snippet only removes it.", function() + feed("ofn") + exec_lua("ls.expand()") + 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:~ }| + {2:-- INSERT --} |]]} + feed("jlafn") + expand() + screen:expect{grid=[[ + | + | + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + ^public void myFunc() { {4:●} | + | + } | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + jump(1) jump(1) + feed("llllvbbbx") + -- first jump goes into function-arguments, second will trigger update, + -- which will in turn recognize the broken snippet. + -- The third jump will then go into the outer snippet. + jump(1) jump(1) jump(-1) + screen:expect{grid=[[ + | + | + /** | + * ^A{3: short Description} | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + c() { | + | + } | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + -- this should jump into the $0 of the outer snippet, highlighting the + -- entire nested snippet. + jump(1) + screen:expect{grid=[[ + | + | + /** | + * A short Description | + */ | + public void myFunc() { | + ^/{3:**} | + {3: * A short Description} | + {3: */} | + {3: c() {} | + {3: } | + {3: }} | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + end) + for _, link_roots_val in ipairs({"true", "false"}) do + it(("Snippets are inserted according to link_roots and keep_roots=%s"):format(link_roots_val), function() + exec_lua(([[ + ls.setup({ + keep_roots = %s, + link_roots = %s + }) + ]]):format(link_roots_val, link_roots_val)) + + feed("ifn") + expand() + -- "o" does not extend the extmark of the active snippet. + feed("Gofn") + expand() + jump(-1) jump(-1) + -- if linked, should end up back in the original snippet, if not, + -- stay in second. + if link_roots_val == "true" then + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + ^ | + } | + | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + else + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + | + ^/** | + * A short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + end + end) + end + for _, keep_roots_val in ipairs({"true", "false"}) do + it("Root-snippets are stored iff keep_roots=true", function() + exec_lua(([[ + ls.setup({ + keep_roots = %s, + }) + ]]):format(keep_roots_val, keep_roots_val)) + + feed("ifn") + expand() + -- "o" does not extend the extmark of the active snippet. + feed("Gofn") + expand() + + -- jump into insert-node in first snippet. + local err = exec_lua( + [[return {pcall(ls.activate_node, {pos = {1, 8}})}]] + )[2] + + -- if linked, should end up back in the original snippet, if not, + -- stay in second. + if keep_roots_val == "true" then + screen:expect{grid=[[ + /** | + * ^A{3: short Description} | + */ | + public void myFunc() { | + | + } | + | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + else + assert( + err:match( + "No Snippet at that position" + ) + ) + end + end) + end + for _, link_children_val in ipairs({"true", "false"}) do + it("Child-snippets are linked iff link_children=true", function() + exec_lua(([[ + ls.setup({ + link_children = %s, + }) + ]]):format(link_children_val)) + + feed("ifn") + expand() + -- expand child-snippet in $0 of original snippet. + feed("jafn") + expand() + -- expand another child. + feed("jjAfn") + expand() + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }/** | + * A short Description | + */ | + ^public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- if linked, should end up back in the original snippet, if not, + -- stay in second. + if link_children_val == "true" then + -- make sure we can jump into the previous child... + jump(-1) jump(-1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + ^ | + }/** | + * A short Description | + */ | + public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- ...and from the first child back into the parent... + jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) + screen:expect{grid=[[ + /** | + * ^A{3: short Description} | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }/** | + * A short Description | + */ | + public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + -- ...and back to the end of the second snippet... + -- (first only almost to the end, to make sure we makde the correct number of jumps). + jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }/** | + * ^A{3: short Description} | + */ | + public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }/** | + * A short Description | + */ | + public void myFunc() { | + ^ | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- test inability to jump beyond a few times, I've had bugs + -- where after a multiple jumps, a new node became active. + jump(1) + screen:expect{unchanged = true} + jump(1) + screen:expect{unchanged = true} + jump(1) + screen:expect{unchanged = true} + + -- For good measure, make sure the node is actually still active. + jump(-1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }/** | + * ^A{3: short Description} | + */ | + public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + else + + end + end) + end + it("Snippets with destroyed extmarks are not used as parents.", function() + feed("ifn") + expand() + -- delete the entier text of a textNode, which will make + -- extmarks_valid() false. + feed("eevllx") + -- insert snippet inside the invalid parent. + feed("jAfn") + expand() + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public voiyFunc() { | + /** | + * 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:~ }| + {2:-- INSERT --} |]]} + + -- make sure the parent is invalid. + jump(-1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public voiyFunc() { | + ^/** | + * A short Description | + */ | + public void myFunc() { | + | + } | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- should not move back into the parent. + jump(-1) + screen:expect{unchanged = true} + end) + it("region_check_events works correctly", function() + exec_lua([[ + ls.setup({ + history = true, + region_check_events = {"CursorHold", "InsertLeave"}, + ext_opts = { + [require("luasnip.util.types").choiceNode] = { + active = { + virt_text = {{"●", "ErrorMsg"}}, + priority = 0 + }, + } + }, + }) + ]]) + + -- expand snippet. + feed("ifn") + expand() + 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 --} |]]} + -- leave its region. + feed("Go") + -- check we have left the snippet (choiceNode indicator no longer active). + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + |]]} + + -- re-activate $0, expand child. + jump(-1) jump(1) + feed("fn") + expand() + + -- jump behind child, activate region_leave, make sure the child and + -- root-snippet are _not_ exited. + feed("jjAo") + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { {4:●} | + | + } | + ^ | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + |]]} + -- .. and now both are left upon leaving the region of the root-snippet. + feed("jji") + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + | + } | + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + |]]} + end) + it("delete_check_events works correctly", function() + exec_lua([[ + ls.setup({ + history = true, + delete_check_events = "TextChanged", + ext_opts = { + [require("luasnip.util.types").choiceNode] = { + active = { + virt_text = {{"●", "ErrorMsg"}}, + priority = 0 + }, + } + }, + }) + ]]) + + -- expand. + feed("ifn") + expand() + 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 --} |]]} + + -- delete textNode, to trigger unlink_current_if_deleted via esc. + feed("eevllx") + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public voi^yFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + |]]} + jump(1) + screen:expect{unchanged=true} + end) + it("Insertion into non-interactive node works correctly", function() + feed("ifn") + expand() + + -- expand snippet in textNode, ie. s.t. it can't be properly linked up. + feed("kifn") + expand() + screen:expect{grid=[[ + /** | + * A short Description | + /** | + * A short Description | + */ | + ^public void myFunc() { {4:●} | + | + } */ | + public void myFunc() { {4:●} | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- jump into startNode, and back into current node. + jump(-1) jump(-1) + screen:expect{grid=[[ + /** | + * A short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } */ | + ^public void myFunc() { {4:●} | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- check jumping out in other direction. + feed("jjifn") + expand() + -- jump to one before jumping out of child-snippet. + jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } */ | + public void myFunc() { {4:●} | + | + /** | + * A short Description | + */ | + public void myFunc() { | + ^ | + }} | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- leave child. + jump(1) + -- check back in current node. + screen:expect{grid=[[ + /** | + * A short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + } */ | + ^public void myFunc() { {4:●} | + | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }} | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + end) + it("All operations work as expected in a longer session.", function() + exec_lua([[ + ls.setup({ + keep_roots = true, + link_roots = true, + link_children = true, + delete_check_events = "TextChanged", + ext_opts = { + [require("luasnip.util.types").choiceNode] = { + active = { + virt_text = {{"●", "ErrorMsg"}}, + priority = 0 + }, + } + }, + }) + ]]) + feed("ifn") + expand() + feed("kkwwwifn") + expand() + screen:expect{grid=[[ + /** | + * A /** | + * A short Description | + */ | + ^public void myFunc() { {4:●} | + | + }short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + feed("ggOfn") + expand() + screen:expect{grid=[[ + /** | + * A short Description | + */ | + ^public void myFunc() { {4:●} | + | + } | + /** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- ensure correct linkage. + jump(1) jump(1) jump(1) + jump(1) jump(1) jump(1) + jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + /** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + */ | + ^public void myFunc() { {4:●} | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- enter third choiceNode of second expanded snippet. + feed("kkkk$h") + exec_lua([[require("luasnip").activate_node()]]) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + /** | + * A /** | + * A short Description | + */ | + public void myFunc()^ { {4:●} | + | + }short Description | + */ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- check connectivity. + jump(1) jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + /** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + */ | + public void myFunc() { | + ^ | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + -- stay at last node. + jump(1) + screen:expect{unchanged = true} + + -- expand in textNode. + feed("kkbifn") + expand() + + -- check connectivity. + jump(-1) jump(-1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + /** | + * ^A{3: /**} | + {3: * A short Description} | + {3: */} | + {3: public void myFunc() {} | + {3: } | + {3: }short Description} | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + + -- end up back in last node, not in textNode-expanded snippet. + jump(1) jump(1) jump(1) jump(1) + jump(1) jump(1) jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A short Description | + */ | + public void myFunc() { | + | + } | + /** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + public void myFunc() { | + ^ | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + feed("gg") + exec_lua([[require("luasnip").activate_node()]]) + + feed("Vjjjjjx") + exec_lua("ls.unlink_current_if_deleted()") + screen:expect{grid=[[ + ^/** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + 6 fewer lines |]]} + -- first snippet is active again. + jump(1) + screen:expect{grid=[[ + /** | + * A /** | + * A short Description | + */ | + public void myFunc() { | + | + }short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + ^public void myFunc() { {4:●} | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + -- make sure the deleted snippet got disconnected properly. + assert.are.same(exec_lua([[return ls.session.current_nodes[1].parent.snippet.prev.prev and "Node before" or "No node before"]]), "No node before") + + -- jump a bit into snippet, so exit_out_of_region changes the current snippet. + jump(1) jump(1) jump(1) jump(1) + jump(1) jump(1) + screen:expect{grid=[[ + /** | + * A /** | + * A short Description | + */ | + ^public void myFunc() { {4:●} | + | + }short Description | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + public void myFunc() { | + | + } | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} |]]} + + feed("Go") + exec_lua("ls.exit_out_of_region(ls.session.current_nodes[1])") + jump(-1) + screen:expect{grid=[[ + /** | + * ^A{3: /**} | + {3: * A short Description} | + {3: */} | + {3: public void myFunc() {} | + {3: } | + {3: }short Description} | + /** | + * A short Description | + */ | + public void myFunc() { | + | + }*/ | + public void myFunc() { | + | + } | + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- SELECT --} |]]} + end) +end) diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index 2989d004a..1cb0ef444 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -238,7 +238,8 @@ describe("snippets_basic", function() unchanged = true, }) - -- jump back before outer. + -- jump back through outer. + -- (can no longer enter it through connections to other snippet) exec_lua("ls.jump(-1)") screen:expect({ grid = [[ @@ -247,15 +248,26 @@ describe("snippets_basic", function() {2:-- INSERT --} |]], unchanged = true, }) - -- the snippet is not active anymore (cursor position doesn't change from last expansion). + -- last snippet is not forgotten (yet). exec_lua("ls.jump(1)") - screen:expect({ - grid = [[ - ^a[a[]ab]ab | + screen:expect{grid=[[ + a[^a{3:[]ab}]ab | {0:~ }| - {2:-- INSERT --} |]], - unchanged = true, - }) + {2:-- SELECT --} |]]} + + feed("o") + exec_lua("ls.snip_expand(" .. snip .. ")") + screen:expect{grid=[[ + a[a[]ab]ab | + a[^]ab | + {2:-- INSERT --} |]]} + exec_lua("ls.jump(-1) ls.jump(-1)") + + -- first snippet can't be accessed anymore. + screen:expect{grid=[[ + a[a[]ab]ab | + ^a[]ab | + {2:-- INSERT --} |]]} end) it("history=true allows jumping back into exited snippet.", function() @@ -576,62 +588,6 @@ describe("snippets_basic", function() ]])) end) - it("{region,delete}_check_events works correctly", function() - exec_lua([[ - ls.setup({ - history = true, - region_check_events = {"CursorHold", "InsertLeave"}, - delete_check_events = "TextChanged,InsertEnter", - }) - - ls.snip_expand(s("a", { - t"sometext", i(1, "someinsertnode") - })) - ]]) - screen:expect({ - grid = [[ - sometext^s{3:omeinsertnode} | - {0:~ }| - {2:-- SELECT --} | -]], - }) - -- leave snippet-area, and trigger insertLeave. - feed("o") - screen:expect({ - grid = [[ - sometextsomeinsertnode | - ^ | - | -]], - }) - -- make sure we're in the last tabstop (ie. region_check_events did its - -- job). - exec_lua("ls.jump(1)") - screen:expect({ - grid = [[ - sometextsomeinsertnode | - ^ | - | -]], - }) - -- not really necessary, but feels safer this way. - exec_lua("ls.jump(-1)") - screen:expect({ - grid = [[ - sometext^s{3:omeinsertnode} | - | - {2:-- SELECT --} | -]], - }) - - -- delete snippet text - feed("dd") - -- make sure the snippet is no longer active. - assert.is_true(exec_lua([[ - return ls.session.current_nodes[vim.api.nvim_get_current_buf()] == nil - ]])) - end) - it("autocommands are registered in different formats", function() local function test_combination(setting_name, overridefn_name) exec_lua(([[ @@ -1244,4 +1200,43 @@ describe("snippets_basic", function() { "𝔼f-𝔼abc" } ) end) + + it("Nested $0 remains active if there is no real next node.", function() + exec_lua([[ + ls.add_snippets("all", { + s("aa", { i(1, "a:"), t"(", i(0), t")" }) + }) + ]]) + + -- expand nested. + feed("iaa") + exec_lua([[ ls.expand() ]]) + exec_lua([[ ls.jump(1) ]]) + screen:expect{grid=[[ + a:(^) | + {0:~ }| + {2:-- INSERT --} |]]} + + feed("aa") + exec_lua([[ ls.expand() ]]) + exec_lua([[ ls.jump(1) ]]) + screen:expect{grid=[[ + a:(a:(^)) | + {0:~ }| + {2:-- INSERT --} |]]} + + feed("aa") + exec_lua([[ ls.expand() ]]) + exec_lua([[ ls.jump(1) ]]) + screen:expect{grid=[[ + a:(a:(a:(^))) | + {0:~ }| + {2:-- INSERT --} |]]} + + -- jump should not move cursor! + -- for some reason need multiple jumps to trigger the mistake. + exec_lua([[ ls.jump(1)]]) + exec_lua([[ ls.jump(1)]]) + screen:expect{unchanged = true} + end) end) From 6e3ba2e0b1c2513915534d03841f3742a3a8e128 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Oct 2023 15:48:51 +0200 Subject: [PATCH 2/7] make `jumpable` smarter. --- lua/luasnip/init.lua | 5 ++-- tests/integration/snippet_basics_spec.lua | 35 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index b71389e5b..7076e0dad 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -126,6 +126,7 @@ local function unlink_current() unlink_set_adjacent_as_current_no_log() 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 @@ -158,8 +159,8 @@ local function jump_destination(dir) end local function jumpable(dir) - local node = session.current_nodes[vim.api.nvim_get_current_buf()] - return (node ~= nil and node:jumpable(dir)) + -- node is jumpable if there is a destination. + return jump_destination(dir) ~= session.current_nodes[vim.api.nvim_get_current_buf()] end local function expandable() diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index 1cb0ef444..b260be202 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -1239,4 +1239,39 @@ describe("snippets_basic", function() exec_lua([[ ls.jump(1)]]) screen:expect{unchanged = true} end) + + it("exit_out_of_region activates last node of snippet-root.", function() + exec_lua([[ + ls.setup({ + link_children = true + }) + + ls.add_snippets("all", { s("aa", { i(1), t"( ", i(0, "0-text"), t" )" }) }) + ]]) + + feed("iaa") + exec_lua("ls.expand()") + feed("lllliaa") + exec_lua("ls.expand()") + exec_lua("ls.jump(-1) ls.jump(-1)") + screen:expect{grid=[[ + ^( 0-( 0-text )text ) | + {0:~ }| + {2:-- INSERT --} |]]} + + feed("o") + exec_lua("ls.exit_out_of_region(ls.session.current_nodes[1])") + + -- verify that we are in the $0 of the nested snippet. + exec_lua("ls.jump(-1)") + screen:expect{grid=[[ + ( 0-^( 0-text )text ) | + | + {2:-- INSERT --} |]]} + exec_lua("ls.jump(1)") + screen:expect{grid=[[ + ( 0-( ^0{3:-text} )text ) | + | + {2:-- SELECT --} |]]} + end) end) From 2cd1f736d64d829bafec1806c7150c03f7e8b598 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Oct 2023 16:20:03 +0200 Subject: [PATCH 3/7] have nested snippets update the node they are inside. --- lua/luasnip/nodes/snippet.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 53eb78f14..72a3f808f 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -358,8 +358,14 @@ local function _S(snip, nodes, opts) -- is propagated to all subsnippets, used to quickly find the outer snippet snip.snippet = snip - -- the snippet may not have dependents. - snip._update_dependents = function() end + -- 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() From 1d4ea786cbb2e82b69a11309347117bb779032eb Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Oct 2023 16:23:06 +0200 Subject: [PATCH 4/7] don't update dynamic/functionNode if their snippet is damaged. --- lua/luasnip/nodes/dynamicNode.lua | 4 ++++ lua/luasnip/nodes/functionNode.lua | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index d97889772..0daad6f20 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -119,6 +119,10 @@ function DynamicNode:update() return end + if not self.parent.snippet:extmarks_valid() then + error("Refusing to update inside a snippet with invalid extmarks") + end + local tmp if self.snip then if not args then diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index 25fd1adb4..cb1a77ae7 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -42,6 +42,11 @@ function FunctionNode:update() if not args or vim.deep_equal(args, self.last_args) then return end + + if not self.parent.snippet:extmarks_valid() then + error("Refusing to update inside a snippet with invalid extmarks") + end + self.last_args = args local text = util.to_string_table(self.fn(args, self.parent, unpack(self.user_args))) From 2bd74f602ea89f7abc59c061d684de476ec2eec7 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Tue, 3 Oct 2023 21:48:42 +0200 Subject: [PATCH 5/7] fix extmark-adjustments for nested snippets. now parents update children, and children parents. --- lua/luasnip/nodes/insertNode.lua | 44 ++++++++++++++++- lua/luasnip/nodes/node.lua | 26 ++++------ lua/luasnip/nodes/snippet.lua | 58 +++++++---------------- lua/luasnip/nodes/util.lua | 53 ++++++++++++++++++++- tests/integration/snippet_basics_spec.lua | 43 +++++++++++++++++ 5 files changed, 164 insertions(+), 60 deletions(-) diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index bd1cae1f4..fc77e756d 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -2,9 +2,9 @@ local Node = require("luasnip.nodes.node") local InsertNode = Node.Node:new() local ExitNode = InsertNode:new() local util = require("luasnip.util.util") +local node_util = require("luasnip.nodes.util") local types = require("luasnip.util.types") local events = require("luasnip.util.events") -local session = require("luasnip.session") local extend_decorator = require("luasnip.util.extend_decorator") local function I(pos, static_text, opts) @@ -279,6 +279,48 @@ function InsertNode:is_interactive() return true end +function InsertNode:child_snippets() + local own_child_snippets = {} + for _, child_snippet in ipairs(self.parent.snippet.child_snippets) do + if child_snippet.parent_node == self then + table.insert(own_child_snippets, child_snippet) + end + end + return own_child_snippets +end + +function InsertNode:subtree_set_pos_rgrav(pos, direction, rgrav) + self.mark:set_rgrav(-direction, rgrav) + + local own_child_snippets = self:child_snippets() + + local child_from_indx + if direction == 1 then + child_from_indx = 1 + else + child_from_indx = #own_child_snippets + end + + node_util.nodelist_adjust_rgravs( + own_child_snippets, + child_from_indx, + pos, + direction, + rgrav, + -- don't assume that the child-snippets are all adjacent. + false) +end + +function InsertNode:subtree_set_rgrav(rgrav) + self.mark:set_rgravs(rgrav, rgrav) + + local own_child_snippets = self:child_snippets() + + for _, child_snippet in ipairs(own_child_snippets) do + child_snippet:subtree_set_rgrav(rgrav) + end +end + return { I = I, } diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 85e1f981a..700696ca4 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -458,6 +458,9 @@ function Node:get_buf_position(opts) end end +-- only does something for insert- and snippetNode. +function Node:set_sibling_rgravs(_, _, _, _) end + -- when an insertNode receives text, its mark/region should contain all the -- text that is inserted. -- This can be achieved by setting the left and right "right-gravity"(rgrav) of @@ -508,15 +511,8 @@ end -- Maybe this whole procedure could be sped up further if we can assume that -- identical endpoints imply identical rgravs. local function focus_node(self, lrgrav, rrgrav) - local abs_pos = vim.deepcopy(self.absolute_position) - -- find nodes on path from self to root. - local nodes_path = node_util.get_nodes_between(self.parent.snippet, self) - -- nodes_on_path_to_self does not include the outer snippet, insert it here - -- (and also insert some dummy-value in abs_pos, such that abs_pos[i] the - -- position of node_path[i] in node_path[i-1] is). - table.insert(nodes_path, 1, self.parent.snippet) - table.insert(abs_pos, 1, 0) + local nodes_path = node_util.root_path(self) -- direction is the direction away from this node, towards the outside of -- the tree-representation of the snippet. @@ -528,7 +524,7 @@ local function focus_node(self, lrgrav, rrgrav) -- adjust left rgrav of all nodes on path upwards to root/snippet: -- (i st. self and the snippet are both handled) - for i = #abs_pos, 1, -1 do + for i = 1, #nodes_path do local node = nodes_path[i] local node_direction_endpoint = node.mark:get_endpoint(direction) @@ -549,17 +545,11 @@ local function focus_node(self, lrgrav, rrgrav) -- dynamicNode, for example, the generated snippets parent is not the -- dynamicNode, but its parent). -- also: don't need to check for nil, because the - local node_above = nodes_path[i - 1] - if - node_above - and ( - node_above.type == types.snippetNode - or node_above.type == types.snippet - ) - then + local node_above = nodes_path[i+1] + if node_above then node_above:set_sibling_rgravs( + node, self_direction_endpoint, - abs_pos[i], direction, direction_rgrav ) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 72a3f808f..230716d1f 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -1297,53 +1297,24 @@ function Snippet:get_keyed_node(key) return self.dependents_dict:get({ "key", key, "node" }) end --- assumption: direction-endpoint of node at child_from_indx is on child_endpoint. --- (caller responsible) -local function adjust_children_rgravs( - self, - child_endpoint, - child_from_indx, - direction, - rgrav -) - local i = child_from_indx - local node = self.nodes[i] - while node do - local direction_node_endpoint = node.mark:get_endpoint(direction) - if util.pos_equal(direction_node_endpoint, child_endpoint) then - -- both endpoints of node are on top of child_endpoint (we wouldn't - -- be in the loop with `node` if the -direction-endpoint didn't - -- match), so update rgravs of the entire subtree to match rgrav - node:subtree_set_rgrav(rgrav) - else - -- only the -direction-endpoint matches child_endpoint, adjust its - -- position and break the loop (don't need to look at any other - -- siblings). - node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav) - break - end - - i = i + direction - node = self.nodes[i] - end -end - -- adjust rgrav of nodes left (direction=-1) or right (direction=1) of node at -- child_indx. -- (direction is the direction into which is searched, from child_indx outward) +-- assumption: direction-endpoint of node is on child_endpoint. (caller +-- responsible) function Snippet:set_sibling_rgravs( + node, child_endpoint, - child_indx, direction, - rgrav -) - adjust_children_rgravs( - self, + rgrav ) + + node_util.nodelist_adjust_rgravs( + self.nodes, + node.absolute_position[#node.absolute_position] + direction, child_endpoint, - child_indx + direction, direction, - rgrav - ) + rgrav, + true) end -- called only if the "-direction"-endpoint has to be changed, but the @@ -1357,7 +1328,14 @@ function Snippet:subtree_set_pos_rgrav(pos, direction, rgrav) else child_from_indx = #self.nodes end - adjust_children_rgravs(self, pos, child_from_indx, direction, rgrav) + + node_util.nodelist_adjust_rgravs( + self.nodes, + child_from_indx, + pos, + direction, + rgrav, + true) end -- changes rgrav of all nodes and all endpoints in this snippetNode to `rgrav`. function Snippet:subtree_set_rgrav(rgrav) diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index 81e752f75..a9a06a580 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -63,6 +63,7 @@ local function wrap_args(args) end end +-- includes child, does not include parent. local function get_nodes_between(parent, child) local nodes = {} @@ -632,6 +633,54 @@ local function snippettree_find_undamaged_node(pos, opts) return prev_parent, prev_parent_children, child_indx, node end +local function root_path(node) + local path = {} + + while node do + local node_snippet = node.parent.snippet + local snippet_node_path = get_nodes_between(node_snippet, node) + -- get_nodes_between gives parent -> node, but we need + -- node -> parent => insert back to front. + for i = #snippet_node_path, 1, -1 do + table.insert(path, snippet_node_path[i]) + end + -- parent not in get_nodes_between. + table.insert(path, node_snippet) + + node = node_snippet.parent_node + end + + return path +end + +-- adjust rgravs of siblings of the node with indx child_from_indx in nodes. +local function nodelist_adjust_rgravs(nodes, child_from_indx, child_endpoint, direction, rgrav, nodes_adjacent) + -- only handle siblings, not the node with child_from_indx itself. + local i = child_from_indx + local node = nodes[i] + while node do + local direction_node_endpoint = node.mark:get_endpoint(direction) + if util.pos_equal(direction_node_endpoint, child_endpoint) then + -- both endpoints of node are on top of child_endpoint (we wouldn't + -- be in the loop with `node` if the -direction-endpoint didn't + -- match), so update rgravs of the entire subtree to match rgrav + node:subtree_set_rgrav(rgrav) + else + -- either assume that they are adjacent, or check. + if nodes_adjacent or util.pos_equal(node.mark:get_endpoint(-direction), child_endpoint) then + -- only the -direction-endpoint matches child_endpoint, adjust its + -- position and break the loop (don't need to look at any other + -- siblings). + node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav) + end + break + end + + i = i + direction + node = nodes[i] + end +end + return { subsnip_init_children = subsnip_init_children, init_child_positions_func = init_child_positions_func, @@ -651,5 +700,7 @@ return { refocus = refocus, generic_extmarks_valid = generic_extmarks_valid, snippettree_find_undamaged_node = snippettree_find_undamaged_node, - interactive_node = interactive_node + interactive_node = interactive_node, + root_path = root_path, + nodelist_adjust_rgravs = nodelist_adjust_rgravs, } diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index b260be202..d27dacd5b 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -1274,4 +1274,47 @@ describe("snippets_basic", function() | {2:-- SELECT --} |]]} end) + + it("focus correctly adjusts gravities of parent-snippets.", function() + exec_lua[[ + ls.setup{ + link_children = true + } + ]] + exec_lua([[ls.lsp_expand("a$1$1a")]]) + exec_lua([[ls.lsp_expand("b$1")]]) + feed("ccc") + exec_lua([[ls.active_update_dependents()]]) + feed("dddd") + -- Here's how this fails if `focus` does not behave correctly (ie. only + -- adjusts extmarks in the snippet the current node is inside): + -- child has a changed $1, triggers update of own snippets, and + -- transitively of the parent-$1. + -- Since the parent has a functionNode that copies the $1's text, it + -- has to first focus the fNode, and update the text. This shifts the + -- gravity of the end of the parent-$1-extmark to the left. + -- Here the first failure may occur: if the child-extmark is not + -- adjusted as well, it will contain the text that belongs to the + -- functionNode. + -- The second issue that may occur is a bit more subtle: + -- After the whole update procedure is done, we have to refocus the + -- current node (since we have to assume that the update changed focus + -- s.t. the current node no longer has correct extmarks). + -- If, in doing this, the parent-$1-extmark end-gravity is not restored + -- to the right, the child-snippet will extend beyond the extmark of + -- its parent-node, the parent-$1. + exec_lua[[ls.jump(-1) ls.jump(-1)]] + -- highlights outer $1. + exec_lua[[ls.jump(1)]] + screen:expect{grid=[[ + a^b{3:cccdddd}bcccdddda | + {0:~ }| + {2:-- SELECT --} |]]} + -- and then inner $1. + exec_lua[[ls.jump(1)]] + screen:expect{grid=[[ + ab^c{3:ccdddd}bcccdddda | + {0:~ }| + {2:-- SELECT --} |]]} + end) end) From 8b2e2961b891671c693b4e06aab1b0c19152dc30 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 4 Oct 2023 13:03:20 +0000 Subject: [PATCH 6/7] Auto generate docs --- doc/luasnip.txt | 74 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index bb06513b5..3a1090a7b 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NVIM v0.8.0 Last change: 2023 October 03 +*luasnip.txt* For NVIM v0.8.0 Last change: 2023 October 04 ============================================================================== Table of Contents *luasnip-table-of-contents* @@ -6,6 +6,7 @@ Table of Contents *luasnip-table-of-contents* 1. Basics |luasnip-basics| - Jump-Index |luasnip-basics-jump-index| - Adding Snippets |luasnip-basics-adding-snippets| + - Snippet Insertion |luasnip-basics-snippet-insertion| 2. Node |luasnip-node| - Api |luasnip-node-api| 3. Snippets |luasnip-snippets| @@ -191,6 +192,41 @@ It is possible to make snippets from one filetype available to another using `ls.filetype_extend`, more info on that in the section |luasnip-api|. +SNIPPET INSERTION *luasnip-basics-snippet-insertion* + +When a new snippet is expanded, it can be connected with the snippets that have +already been expanded in the buffer in various ways. First of all, Luasnip +distinguishes between root-snippets and child-snippets. The latter are nested +inside other snippets, so when jumping through a snippet, one may also traverse +the child-snippets expanded inside it, more or less as if the child just +contains more nodes of the parent. Root-snippets are of course characterised by +not being child-snippets. When expanding a new snippet, it becomes a child of +the snippet whose region it is expanded inside, and a root if it is not inside +any snippet’s region. If it is inside another snippet, the specific node it +is inside is determined, and the snippet then nested inside that node. * If +that node is interactive (for example, an `insertNode`), the new snippet will +be traversed when the node is visited, as long as the configuration-option +`link_children` is enabled. If it is not enabled, it is possible to jump from +the snippet to the node, but not the other way around. * If that node is not +interactive, the snippet will be linked to the currently active node, also such +that it will not be jumped to again once it is left. This is to prevent jumping +large distances across the buffer as much as possible. There may still be one +large jump from the snippet back to the current node it is nested inside, but +that seems hard to avoid. Thus, one should design snippets such that the +regions where other snippets may be expanded are inside `insertNodes`. + +If the snippet is not a child, but a root, it can be linked up with the roots +immediately adjacent to it by enabling `link_roots` in `setup`. Since by +default only one root is remembered, one should also set `keep_roots` if +`link_roots` is enabled. The two are separate options, since roots that are not +linked can still be reached by `ls.activate_node()`. This setup (remember +roots, but don’t jump to them) is useful for a super-tab like mapping +(`` and jump on the same key), where one would like to still enter +previous roots. Since there would almost always be more jumps if the roots are +linked, regular `` would not work almost all the time, and thus +`link_roots` has to stay disabled. + + ============================================================================== 2. Node *luasnip-node* @@ -3226,10 +3262,15 @@ It is also possible to get/set the source of a snippet via API: These are the settings you can provide to `luasnip.setup()`: -- `history`: If true, snippets that were exited can still be jumped back into. As - snippets are not removed when their text is deleted, they have to be removed - manually via `LuasnipUnlinkCurrent` if `delete_check_events` is not enabled - (set to eg. `'TextChanged'`). +- `keep_roots`: Whether snippet-roots should be linked. See + |luasnip-basics-snippet-insertion| for more context. +- `link_roots`: Whether snippet-roots should be linked. See + |luasnip-basics-snippet-insertion| for more context. +- `link_children`: Whether children should be linked. See + |luasnip-basics-snippet-insertion| for more context. +- `history` (deprecated): if not nil, `keep_roots`, `link_roots`, and + `link_children` will bet set to the value of `history`. This is just to ensure + backwards-compatibility. - `update_events`: Choose which events trigger an update of the active nodes’ dependents. Default is just `'InsertLeave'`, `'TextChanged,TextChangedI'` would update on every change. These, like all other `*_events` are passed to @@ -3239,7 +3280,7 @@ These are the settings you can provide to `luasnip.setup()`: update_events = {"TextChanged", "TextChangedI"} }) < -- `region_check_events`: Events on which to leave the current snippet if the +- `region_check_events`: Events on which to leave the current snippet-root if the cursor is outside its’ 'region'. Disabled by default, `'CursorMoved'`, `'CursorHold'` or `'InsertEnter'` seem reasonable. - `delete_check_events`: When to check if the current snippet was deleted, and if @@ -3464,13 +3505,11 @@ GENERAL ~ You can use it for more granular control over the table of snippets that is returned. - `exit_out_of_region(node)`: checks whether the cursor is still within the range - of the snippet `node` belongs to. If yes, no change occurs; if no, the snippet - is exited and following snippets’ regions are checked and potentially exited - (the next active node will be the 0-node of the snippet before the one the - cursor is inside. If the cursor isn’t inside any snippet, the active node - will be the last node in the jumplist). If a jump causes an error (happens - mostly because a snippet was deleted), the snippet is removed from the - jumplist. + of the root-snippet `node` belongs to. If yes, no change occurs; if no, the + root-snippet is exited and its `$0` will be the new active node. If a jump + causes an error (happens mostly because the text of a snippet was deleted), the + snippet is removed from the jumplist and the current node set to the + end/beginning of the next/previous snippet. - `store_snippet_docstrings(snippet_table)`: Stores the docstrings of all snippets in `snippet_table` to a file (`stdpath("cache")/luasnip/docstrings.json`). Calling @@ -3518,6 +3557,15 @@ GENERAL ~ (either -1 or 1, for backwards, forwards respectively) leads to, or `nil` if the destination could not be determined (most likely because there is no node that can be jumped to in the given direction, or there is no active node). +- `activate_node(opts)`: Activate a node in any snippet. `opts` contains the + following options: + - `pos`, `{[1]: row, [2]: byte-column}?`: The position at which a node should + be activated. Defaults to the position of the cursor. + - `strict`, `bool?`: If set, throw an error if the node under the cursor can’t + be jumped into. If not set, fall back to any node of the snippet and enter + that instead. + - `select`, `bool?`: Whether the text inside the node should be selected. + Defaults to true. Not covered in this section are the various node-constructors exposed by the module, their usage is shown either previously in this file or in From 3788ef48c32a7b052ef1ecd6004d57cdb6cdebf9 Mon Sep 17 00:00:00 2001 From: L3MON4D3 Date: Wed, 4 Oct 2023 13:03:25 +0000 Subject: [PATCH 7/7] Format with stylua --- lua/luasnip/init.lua | 69 +++- lua/luasnip/nodes/insertNode.lua | 12 +- lua/luasnip/nodes/node.lua | 17 +- lua/luasnip/nodes/snippet.lua | 70 ++-- lua/luasnip/nodes/textNode.lua | 8 +- lua/luasnip/nodes/util.lua | 102 +++-- lua/luasnip/session/init.lua | 4 +- lua/luasnip/util/util.lua | 2 +- tests/helpers.lua | 6 +- tests/integration/session_spec.lua | 439 +++++++++++++++------- tests/integration/snippet_basics_spec.lua | 80 ++-- 11 files changed, 557 insertions(+), 252 deletions(-) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 7076e0dad..5e9192d95 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -97,7 +97,9 @@ local function unlink_set_adjacent_as_current_no_log(snippet) if next_current then -- if snippet was active before, we need to now set its parent to be no -- longer inner_active. - if snippet.parent_node == next_current and next_current.inner_active then + if + snippet.parent_node == next_current and next_current.inner_active + then snippet.parent_node:input_leave_children() else -- set no_move. @@ -106,7 +108,11 @@ local function unlink_set_adjacent_as_current_no_log(snippet) -- this won't try to set the previously broken snippet as -- current, since that link is removed in -- `remove_from_jumplist`. - unlink_set_adjacent_as_current(next_current.parent.snippet, "Error while setting adjacent snippet as current node: %s", err) + unlink_set_adjacent_as_current( + next_current.parent.snippet, + "Error while setting adjacent snippet as current node: %s", + err + ) end end end @@ -139,7 +145,12 @@ local function safe_jump_current(dir, no_move, dry_run) else local snip = node.parent.snippet - unlink_set_adjacent_as_current(snip, "Removing snippet `%s` due to error %s", snip.trigger, res) + unlink_set_adjacent_as_current( + snip, + "Removing snippet `%s` due to error %s", + snip.trigger, + res + ) return session.current_nodes[vim.api.nvim_get_current_buf()] end end @@ -160,7 +171,8 @@ end local function jumpable(dir) -- node is jumpable if there is a destination. - return jump_destination(dir) ~= session.current_nodes[vim.api.nvim_get_current_buf()] + return jump_destination(dir) + ~= session.current_nodes[vim.api.nvim_get_current_buf()] end local function expandable() @@ -186,7 +198,11 @@ local function in_snippet() -- if there was an error getting the position, the snippets text was -- most likely removed, resulting in messed up extmarks -> error. -- remove the snippet. - unlink_set_adjacent_as_current(snippet, "Error while getting extmark-position: %s", snip_begin_pos) + unlink_set_adjacent_as_current( + snippet, + "Error while getting extmark-position: %s", + snip_begin_pos + ) return end local pos = vim.api.nvim_win_get_cursor(0) @@ -260,7 +276,8 @@ local function snip_expand(snippet, opts) session.current_nodes[vim.api.nvim_get_current_buf()] = opts.jump_into_func(snip) - local buf_snippet_roots = session.snippet_roots[vim.api.nvim_get_current_buf()] + local buf_snippet_roots = + session.snippet_roots[vim.api.nvim_get_current_buf()] if not session.config.keep_roots and #buf_snippet_roots > 1 then -- if history is not set, and there is more than one snippet-root, -- remove the other one. @@ -395,7 +412,12 @@ local function safe_choice_action(snip, ...) else -- not very elegant, but this way we don't have a near -- re-implementation of unlink_current. - unlink_set_adjacent_as_current(snip, "Removing snippet `%s` due to error %s", snip.trigger, res) + unlink_set_adjacent_as_current( + snip, + "Removing snippet `%s` due to error %s", + snip.trigger, + res + ) return session.current_nodes[vim.api.nvim_get_current_buf()] end end @@ -464,19 +486,21 @@ local function active_update_dependents() local ok, err = pcall(active.update_dependents, active) if not ok then - log.warn( - ) - unlink_set_adjacent_as_current(active.parent.snippet, + log.warn() + unlink_set_adjacent_as_current( + active.parent.snippet, "Error while updating dependents for snippet %s due to error %s", active.parent.snippet.trigger, - err) + 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, + unlink_set_adjacent_as_current( + active.parent.snippet, "Error while entering node in snippet %s: %s", active.parent.snippet.trigger, err @@ -578,7 +602,11 @@ local function unlink_current_if_deleted() -- * textnodes that should contain text still do so, and -- * that extmarks still fulfill all expectations (should be successive, no gaps, etc.) if not snippet:extmarks_valid() then - unlink_set_adjacent_as_current(snippet, "Detected deletion of snippet `%s`, removing it", snippet.trigger) + unlink_set_adjacent_as_current( + snippet, + "Detected deletion of snippet `%s`, removing it", + snippet.trigger + ) end end @@ -605,7 +633,11 @@ local function exit_out_of_region(node) pcall(snippet.mark.pos_begin_end, snippet.mark) if not ok then - unlink_set_adjacent_as_current(snippet, "Error while getting extmark-position: %s", snip_begin_pos) + unlink_set_adjacent_as_current( + snippet, + "Error while getting extmark-position: %s", + snip_begin_pos + ) return end @@ -744,7 +776,7 @@ local function activate_node(opts) local _, _, _, node = node_util.snippettree_find_undamaged_node(pos, { tree_respect_rgravs = false, tree_preference = node_util.binarysearch_preference.inside, - snippet_mode = "interactive" + snippet_mode = "interactive", }) if not node then @@ -774,7 +806,10 @@ local function activate_node(opts) end end - node_util.refocus(session.current_nodes[vim.api.nvim_get_current_buf()], node) + node_util.refocus( + session.current_nodes[vim.api.nvim_get_current_buf()], + node + ) if select then -- input_enter node again, to get highlight and the like. -- One side-effect of this is that an event will be execute twice, but I @@ -856,7 +891,7 @@ ls = util.lazy_table({ setup = require("luasnip.config").setup, extend_decorator = extend_decorator, log = require("luasnip.util.log"), - activate_node = activate_node + activate_node = activate_node, }, ls_lazy) return ls diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index fc77e756d..b8b456fc5 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -187,7 +187,8 @@ function ExitNode:jump_from(dir, no_move, dry_run) self:init_dry_run_inner_active(dry_run) local next_node = util.ternary(dir == 1, self.next, self.prev) - local next_inner_node = util.ternary(dir == 1, self.inner_first, self.inner_last) + local next_inner_node = + util.ternary(dir == 1, self.inner_first, self.inner_last) if next_inner_node then self:input_enter_children(dry_run) @@ -199,7 +200,8 @@ function ExitNode:jump_from(dir, no_move, dry_run) -- not have children active if jump_from is called. -- true: don't move - local target_node = next_node:jump_into(dir, true, next_node_dry_run) + local target_node = + next_node:jump_into(dir, true, next_node_dry_run) -- if there is no node that can serve as jump-target, just remain -- here. -- Regular insertNodes don't have to handle this, since there is @@ -220,7 +222,8 @@ function InsertNode:jump_from(dir, no_move, dry_run) self:init_dry_run_inner_active(dry_run) local next_node = util.ternary(dir == 1, self.next, self.prev) - local next_inner_node = util.ternary(dir == 1, self.inner_first, self.inner_last) + local next_inner_node = + util.ternary(dir == 1, self.inner_first, self.inner_last) if next_inner_node then self:input_enter_children(dry_run) @@ -308,7 +311,8 @@ function InsertNode:subtree_set_pos_rgrav(pos, direction, rgrav) direction, rgrav, -- don't assume that the child-snippets are all adjacent. - false) + false + ) end function InsertNode:subtree_set_rgrav(rgrav) diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 700696ca4..c6630c8ec 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -545,7 +545,7 @@ local function focus_node(self, lrgrav, rrgrav) -- dynamicNode, for example, the generated snippets parent is not the -- dynamicNode, but its parent). -- also: don't need to check for nil, because the - local node_above = nodes_path[i+1] + local node_above = nodes_path[i + 1] if node_above then node_above:set_sibling_rgravs( node, @@ -603,14 +603,23 @@ end function Node:linkable() -- linkable if insert or exitNode. - return vim.tbl_contains({types.insertNode, types.exitNode}, rawget(self, "type")) + return vim.tbl_contains( + { types.insertNode, types.exitNode }, + rawget(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 + return vim.tbl_contains( + { types.insertNode, types.exitNode }, + rawget(self, "type") + ) or rawget(self, "choice") ~= nil end function Node:leaf() - return vim.tbl_contains({types.textNode, types.functionNode, types.insertNode, types.exitNode}, rawget(self, "type")) + return vim.tbl_contains( + { types.textNode, types.functionNode, types.insertNode, types.exitNode }, + rawget(self, "type") + ) end return { diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 230716d1f..e81fea461 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -350,7 +350,7 @@ local function _S(snip, nodes, opts) -- list of snippets expanded within the region of this snippet. -- sorted by their buffer-position, for quick searching. - child_snippets = {} + child_snippets = {}, }), opts ) @@ -487,7 +487,9 @@ function Snippet:remove_from_jumplist() self:exit() - local sibling_list = self.parent_node ~= nil and self.parent_node.parent.snippet.child_snippets or session.snippet_roots[vim.api.nvim_get_current_buf()] + local sibling_list = self.parent_node ~= nil + and self.parent_node.parent.snippet.child_snippets + or session.snippet_roots[vim.api.nvim_get_current_buf()] local self_indx for i, snip in ipairs(sibling_list) do if snip == self then @@ -523,8 +525,15 @@ function Snippet:remove_from_jumplist() end end -local function insert_into_jumplist(snippet, start_node, current_node, parent_node, sibling_snippets, own_indx) - local prev_snippet = sibling_snippets[own_indx-1] +local function insert_into_jumplist( + snippet, + start_node, + current_node, + parent_node, + sibling_snippets, + own_indx +) + local prev_snippet = sibling_snippets[own_indx - 1] -- have not yet inserted self!! local next_snippet = sibling_snippets[own_indx] @@ -620,11 +629,12 @@ function Snippet:trigger_expand(current_node, pos_id, env) local pos = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, pos_id, {}) -- find tree-node the snippet should be inserted at (could be before another node). - local _, sibling_snippets, own_indx, parent_node = node_util.snippettree_find_undamaged_node(pos, { - tree_respect_rgravs = false, - tree_preference = node_util.binarysearch_preference.outside, - snippet_mode = "linkable" - }) + local _, sibling_snippets, own_indx, parent_node = + node_util.snippettree_find_undamaged_node(pos, { + tree_respect_rgravs = false, + tree_preference = node_util.binarysearch_preference.outside, + snippet_mode = "linkable", + }) if current_node then if parent_node then @@ -718,7 +728,7 @@ function Snippet:trigger_expand(current_node, pos_id, env) 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} + start_node.absolute_position = { -1 } start_node.parent = self -- hook up i0 and start_node, and then the snippet itself. @@ -734,7 +744,14 @@ function Snippet:trigger_expand(current_node, pos_id, env) -- parent_node is nil if the snippet is toplevel. self.parent_node = parent_node - insert_into_jumplist(self, start_node, current_node, parent_node, sibling_snippets, own_indx) + insert_into_jumplist( + self, + start_node, + current_node, + parent_node, + sibling_snippets, + own_indx + ) return parent_node end @@ -1302,19 +1319,15 @@ end -- (direction is the direction into which is searched, from child_indx outward) -- assumption: direction-endpoint of node is on child_endpoint. (caller -- responsible) -function Snippet:set_sibling_rgravs( - node, - child_endpoint, - direction, - rgrav ) - +function Snippet:set_sibling_rgravs(node, child_endpoint, direction, rgrav) node_util.nodelist_adjust_rgravs( self.nodes, node.absolute_position[#node.absolute_position] + direction, child_endpoint, direction, rgrav, - true) + true + ) end -- called only if the "-direction"-endpoint has to be changed, but the @@ -1335,7 +1348,8 @@ function Snippet:subtree_set_pos_rgrav(pos, direction, rgrav) pos, direction, rgrav, - true) + true + ) end -- changes rgrav of all nodes and all endpoints in this snippetNode to `rgrav`. function Snippet:subtree_set_rgrav(rgrav) @@ -1383,13 +1397,13 @@ function Snippet:node_at(pos, mode) -- all nodes well before it are quickly skipped => should benefit -- all cases where the runtime of this is noticeable, and which are not -- unrealistic (lots of zero-width nodes). - if util.pos_cmp(pos, {node_to[1], node_to[2]+1}) > 0 then + if util.pos_cmp(pos, { node_to[1], node_to[2] + 1 }) > 0 then return false end -- generate gravity-adjusted endpoints. - local grav_adjusted_from = {node_from[1], node_from[2]} - local grav_adjusted_to = {node_to[1], node_to[2]} + local grav_adjusted_from = { node_from[1], node_from[2] } + local grav_adjusted_to = { node_to[1], node_to[2] } if node_mark:get_rgrav(-1) then grav_adjusted_from[2] = grav_adjusted_from[2] + 1 end @@ -1453,7 +1467,8 @@ end function Snippet:extmarks_valid() -- assumption: extmarks are contiguous, and all can be queried via pos_begin_end_raw. - local ok, current_from, self_to = pcall(self.mark.pos_begin_end_raw, self.mark) + local ok, current_from, self_to = + pcall(self.mark.pos_begin_end_raw, self.mark) if not ok then return false end @@ -1464,12 +1479,17 @@ function Snippet:extmarks_valid() end for _, node in ipairs(self.nodes) do - local ok_, node_from, node_to = pcall(node.mark.pos_begin_end_raw, node.mark) + local ok_, node_from, node_to = + 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 -- - any child of this node violates these rules. - if not ok_ or util.pos_cmp(current_from, node_from) ~= 0 or not node:extmarks_valid() then + if + not ok_ + or util.pos_cmp(current_from, node_from) ~= 0 + or not node:extmarks_valid() + then return false end current_from = node_to diff --git a/lua/luasnip/nodes/textNode.lua b/lua/luasnip/nodes/textNode.lua index 6994f9f7d..88fad040d 100644 --- a/lua/luasnip/nodes/textNode.lua +++ b/lua/luasnip/nodes/textNode.lua @@ -49,7 +49,13 @@ end function TextNode:extmarks_valid() local from, to = self.mark:pos_begin_end_raw() - if util.pos_cmp(from, to) == 0 and not (#self.static_text == 0 or (#self.static_text == 1 and #self.static_text[1] == 0)) then + if + util.pos_cmp(from, to) == 0 + and not ( + #self.static_text == 0 + or (#self.static_text == 1 and #self.static_text[1] == 0) + ) + then -- assume the snippet is invalid if a textNode occupies zero space, -- but has text which would occupy some. -- This should allow some modifications, but as soon as a textNode is diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index a9a06a580..e17f1589e 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -104,7 +104,7 @@ local function leave_nodes_between(parent, child, no_move) -- entirely (because we stop at nodes[2], and handle nodes[1] -- separately) nodes[i]:input_leave(no_move) - nodes[i-1]:input_leave_children() + nodes[i - 1]:input_leave_children() end nodes[1]:input_leave(no_move) end @@ -115,7 +115,7 @@ local function enter_nodes_between(parent, child, no_move) return end - for i = 1, #nodes-1 do + for i = 1, #nodes - 1 do -- only enter children for nodes before the last (lowest) one. nodes[i]:input_enter(no_move) nodes[i]:input_enter_children() @@ -180,7 +180,10 @@ end 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")) + return vim.tbl_contains( + { types.insertNode, types.exitNode }, + rawget(node, "type") + ) end -- mainly used internally, by binarysearch_pos. @@ -190,7 +193,10 @@ 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}, rawget(node, "type")) + return vim.tbl_contains( + { types.textNode, types.functionNode }, + rawget(node, "type") + ) end -- return whether a node is certainly (not) interactive. -- Coincindentially, the same nodes as (non-)linkable ones, but since there is a @@ -228,7 +234,7 @@ local binarysearch_preference = { return cmp_mid_to > 0, cmp_mid_from < 0 end, linkable = prefer_nodes(linkable_node, non_linkable_node), - interactive = prefer_nodes(interactive_node, non_interactive_node) + interactive = prefer_nodes(interactive_node, non_interactive_node), } -- `nodes` is a list of nodes ordered by their occurrence in the buffer. -- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT @@ -256,12 +262,12 @@ local binarysearch_preference = { -- This way, we are more likely to return a node that can handle a new -- snippet/is interactive. -- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd --- like to find a position between two nodes. +-- like to find a position between two nodes. -- This mode makes sense for finding the snippet a new snippet should be -- inserted in, since we'd like to prefer inserting before/after a snippet, if -- the position is ambiguous. --- --- In general: +-- +-- In general: -- These options are useful for making this function more general: When -- searching in the contiguous nodes of a snippet, we'd like this routine to -- return any of them (obviously the one pos is inside/or on the border of, and @@ -271,7 +277,12 @@ local binarysearch_preference = { -- the snippet/node a new snippet should be expanded inside, it seems better to -- shift an existing snippet to the right/left than expand the new snippet -- inside it (when the expand-point is on the boundary). -local function binarysearch_pos(nodes, pos, respect_rgravs, boundary_resolve_mode) +local function binarysearch_pos( + nodes, + pos, + respect_rgravs, + boundary_resolve_mode +) local left = 1 local right = #nodes @@ -281,7 +292,7 @@ local function binarysearch_pos(nodes, pos, respect_rgravs, boundary_resolve_mod return nil, 1 end while true do - local mid = left + math.floor((right-left)/2) + local mid = left + math.floor((right - left) / 2) local mid_mark = nodes[mid].mark local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark) @@ -310,7 +321,8 @@ local function binarysearch_pos(nodes, pos, respect_rgravs, boundary_resolve_mod local cmp_mid_to = util.pos_cmp(pos, mid_to) local cmp_mid_from = util.pos_cmp(pos, mid_from) - local cont_behind_mid, cont_before_mid = boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid]) + local cont_behind_mid, cont_before_mid = + boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid]) if cont_behind_mid then -- make sure right-left becomes smaller. @@ -342,7 +354,7 @@ local function first_common_node(a, b) local i = 0 local last_common = a.parent.snippet -- invariant: last_common is parent of both a and b. - while (a_pos[i+1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do + while (a_pos[i + 1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do last_common = last_common:resolve_position(a_pos[i + 1]) i = i + 1 end @@ -455,7 +467,11 @@ local function refocus(from, to) end -- pass nil if from/to is nil. -- if either is nil, first_common_node is nil, and the corresponding list empty. - local first_common_snippet, from_snip_path, to_snip_path = first_common_snippet_ancestor_path(from and from.parent.snippet, to and to.parent.snippet) + local first_common_snippet, from_snip_path, to_snip_path = + first_common_snippet_ancestor_path( + from and from.parent.snippet, + to and to.parent.snippet + ) -- we want leave/enter_path to be s.t. leaving/entering all nodes between -- each entry and its snippet (and the snippet itself) will leave/enter all @@ -498,7 +514,8 @@ local function refocus(from, to) -- here. -- snippet does not have input_leave_children, so only input_leave -- needs to be called. - local ok2 = pcall(from.parent.snippet.input_leave, from.parent.snippet, true) + local ok2 = + pcall(from.parent.snippet.input_leave, from.parent.snippet, true) if not ok1 or not ok2 then from.parent.snippet:remove_from_jumplist() end @@ -507,7 +524,8 @@ local function refocus(from, to) local node = from_snip_path[i] local ok1 = pcall(node.input_leave_children, node) local ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true) - local ok3 = pcall(node.parent.snippet.input_leave, node.parent.snippet, true) + local ok3 = + pcall(node.parent.snippet.input_leave, node.parent.snippet, true) if not ok1 or not ok2 or not ok3 then from.parent.snippet:remove_from_jumplist() end @@ -533,11 +551,17 @@ local function refocus(from, to) -- This means that, if we want to enter a non-exitNode, we have to -- explicitly activate the snippet for all jumps to behave correctly. -- (if we enter a i(0)/i(-1), this is not necessary, of course). - if final_leave_node.type == types.exitNode and first_enter_node.type ~= types.exitNode then + if + final_leave_node.type == types.exitNode + and first_enter_node.type ~= types.exitNode + then common_node:input_enter(true) end -- symmetrically, entering an i(0)/i(-1) requires leaving the snippet. - if final_leave_node.type ~= types.exitNode and first_enter_node.type == types.exitNode then + if + final_leave_node.type ~= types.exitNode + and first_enter_node.type == types.exitNode + then common_node:input_leave(true) end @@ -579,10 +603,17 @@ local function generic_extmarks_valid(node, child) -- valid if -- - extmark-extents match. -- - current choice is valid - local ok1, self_from, self_to = pcall(node.mark.pos_begin_end_raw, node.mark) - local ok2, child_from, child_to = pcall(child.mark.pos_begin_end_raw, child.mark) - - if not ok1 or not ok2 or util.pos_cmp(self_from, child_from) ~= 0 or util.pos_cmp(self_to, child_to) ~= 0 then + local ok1, self_from, self_to = + pcall(node.mark.pos_begin_end_raw, node.mark) + local ok2, child_from, child_to = + pcall(child.mark.pos_begin_end_raw, child.mark) + + if + not ok1 + or not ok2 + or util.pos_cmp(self_from, child_from) ~= 0 + or util.pos_cmp(self_to, child_to) ~= 0 + then return false end return child:extmarks_valid() @@ -595,19 +626,25 @@ end -- * the node of this snippet pos is on. local function snippettree_find_undamaged_node(pos, opts) local prev_parent, child_indx, found_parent - local prev_parent_children = session.snippet_roots[vim.api.nvim_get_current_buf()] + local prev_parent_children = + session.snippet_roots[vim.api.nvim_get_current_buf()] while true do -- false: don't respect rgravs. -- Prefer inserting the snippet outside an existing one. - found_parent, child_indx = binarysearch_pos(prev_parent_children, pos, opts.tree_respect_rgravs, opts.tree_preference) + found_parent, child_indx = binarysearch_pos( + prev_parent_children, + pos, + opts.tree_respect_rgravs, + opts.tree_preference + ) if found_parent == false then -- if the procedure returns false, there was an error getting the -- position of a node (in this case, that node is a snippet). -- 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() - elseif (found_parent ~= nil and not found_parent:extmarks_valid()) then + 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 -- input_enter all the nodes along the insertion-path correctly). @@ -654,7 +691,14 @@ local function root_path(node) end -- adjust rgravs of siblings of the node with indx child_from_indx in nodes. -local function nodelist_adjust_rgravs(nodes, child_from_indx, child_endpoint, direction, rgrav, nodes_adjacent) +local function nodelist_adjust_rgravs( + nodes, + child_from_indx, + child_endpoint, + direction, + rgrav, + nodes_adjacent +) -- only handle siblings, not the node with child_from_indx itself. local i = child_from_indx local node = nodes[i] @@ -667,7 +711,13 @@ local function nodelist_adjust_rgravs(nodes, child_from_indx, child_endpoint, di node:subtree_set_rgrav(rgrav) else -- either assume that they are adjacent, or check. - if nodes_adjacent or util.pos_equal(node.mark:get_endpoint(-direction), child_endpoint) then + if + nodes_adjacent + or util.pos_equal( + node.mark:get_endpoint(-direction), + child_endpoint + ) + then -- only the -direction-endpoint matches child_endpoint, adjust its -- position and break the loop (don't need to look at any other -- siblings). diff --git a/lua/luasnip/session/init.lua b/lua/luasnip/session/init.lua index 77447c3ce..52b3b2fbf 100644 --- a/lua/luasnip/session/init.lua +++ b/lua/luasnip/session/init.lua @@ -17,11 +17,11 @@ M.current_nodes = {} -- snippet_roots[n] => list of snippet-roots in buffer n. M.snippet_roots = setmetatable({}, { -- create missing lists automatically. - __index = function(t,k) + __index = function(t, k) local new_t = {} rawset(t, k, new_t) return new_t - end + end, }) M.ns_id = vim.api.nvim_create_namespace("Luasnip") M.active_choice_nodes = {} diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 1d625e82d..3018278a8 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -627,7 +627,7 @@ end -- compare two positions, <0 => pos1 pos1=pos2, >0 => pos1 > pos2. local function pos_cmp(pos1, pos2) -- if row is different it determines result, otherwise the column does. - return 2*cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2]) + return 2 * cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2]) end return { diff --git a/tests/helpers.lua b/tests/helpers.lua index 3b89a3d24..bbfd2a86f 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -67,7 +67,8 @@ function M.session_setup_luasnip(opts) ]]) end - helpers.exec_lua([[ + helpers.exec_lua( + [[ local hl_choiceNode, setup_extend = ... -- MYVIMRC might not be set when nvim is loaded like this. @@ -89,7 +90,8 @@ function M.session_setup_luasnip(opts) ]], -- passing nil here means the argument-list is terminated, I think. -- Just pass false instead of nil/false. - hl_choiceNode or false, setup_extend + hl_choiceNode or false, + setup_extend ) if not no_snip_globals then diff --git a/tests/integration/session_spec.lua b/tests/integration/session_spec.lua index e4d44e621..d4760efc0 100644 --- a/tests/integration/session_spec.lua +++ b/tests/integration/session_spec.lua @@ -5,9 +5,15 @@ local exec_lua, feed, exec = helpers.exec_lua, helpers.feed, helpers.exec local ls_helpers = require("helpers") local Screen = require("test.functional.ui.screen") -local function expand() exec_lua("ls.expand()") end -local function jump(dir) exec_lua("ls.jump(...)", dir) end -local function change(dir) exec_lua("ls.change_choice(...)", dir) end +local function expand() + exec_lua("ls.expand()") +end +local function jump(dir) + exec_lua("ls.jump(...)", dir) +end +local function change(dir) + exec_lua("ls.change_choice(...)", dir) +end describe("session", function() local screen @@ -15,7 +21,7 @@ describe("session", function() before_each(function() helpers.clear() ls_helpers.setup_jsregexp() - ls_helpers.session_setup_luasnip({hl_choiceNode = true}) + ls_helpers.session_setup_luasnip({ hl_choiceNode = true }) -- add a rather complicated snippet. -- It may be a bit hard to grasp, but will cover lots and lots of @@ -133,14 +139,18 @@ describe("session", function() [1] = { bold = true, foreground = Screen.colors.Brown }, [2] = { bold = true }, [3] = { background = Screen.colors.LightGray }, - [4] = {background = Screen.colors.Red1, foreground = Screen.colors.White} + [4] = { + background = Screen.colors.Red1, + foreground = Screen.colors.White, + }, }) end) it("Deleted snippet is handled properly in expansion.", function() feed("ofn") exec_lua("ls.expand()") - screen:expect{grid=[[ + screen:expect({ + grid = [[ | | /** | @@ -170,9 +180,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} - jump(1) jump(1) jump(1) - screen:expect{grid=[[ + {2:-- INSERT --} |]], + }) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ | | /** | @@ -202,15 +216,18 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- delete whole buffer. feed("ggVGcfn") -- immediately expand at the old position of the snippet. exec_lua("ls.expand()") -- first jump goes to i(-1), second might go back into deleted snippet, -- if we did something wrong. - jump(-1) jump(-1) - screen:expect{grid=[[ + jump(-1) + jump(-1) + screen:expect({ + grid = [[ ^/** | * A short Description | */ | @@ -240,11 +257,18 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- seven jumps to go to i(0), 8th, again, should not do anything. - jump(1) jump(1) jump(1) jump(1) - jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -274,14 +298,16 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) jump(1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) end) it("Deleted snippet is handled properly when jumping.", function() feed("ofn") exec_lua("ls.expand()") - screen:expect{grid=[[ + screen:expect({ + grid = [[ | | /** | @@ -311,9 +337,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} - jump(1) jump(1) jump(1) - screen:expect{grid=[[ + {2:-- INSERT --} |]], + }) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ | | /** | @@ -343,7 +373,8 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- delete whole buffer. feed("ggVGd") -- should not cause an error. @@ -352,7 +383,8 @@ describe("session", function() it("Deleting nested snippet only removes it.", function() feed("ofn") exec_lua("ls.expand()") - screen:expect{grid=[[ + screen:expect({ + grid = [[ | | /** | @@ -382,10 +414,12 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("jlafn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ | | /** | @@ -415,14 +449,19 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} - jump(1) jump(1) + {2:-- INSERT --} |]], + }) + jump(1) + jump(1) feed("llllvbbbx") -- first jump goes into function-arguments, second will trigger update, -- which will in turn recognize the broken snippet. -- The third jump will then go into the outer snippet. - jump(1) jump(1) jump(-1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(-1) + screen:expect({ + grid = [[ | | /** | @@ -452,11 +491,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) -- this should jump into the $0 of the outer snippet, highlighting the -- entire nested snippet. jump(1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ | | /** | @@ -486,27 +527,34 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) end) - for _, link_roots_val in ipairs({"true", "false"}) do - it(("Snippets are inserted according to link_roots and keep_roots=%s"):format(link_roots_val), function() - exec_lua(([[ + for _, link_roots_val in ipairs({ "true", "false" }) do + it( + ("Snippets are inserted according to link_roots and keep_roots=%s"):format( + link_roots_val + ), + function() + exec_lua(([[ ls.setup({ keep_roots = %s, link_roots = %s }) ]]):format(link_roots_val, link_roots_val)) - feed("ifn") - expand() - -- "o" does not extend the extmark of the active snippet. - feed("Gofn") - expand() - jump(-1) jump(-1) - -- if linked, should end up back in the original snippet, if not, - -- stay in second. - if link_roots_val == "true" then - screen:expect{grid=[[ + feed("ifn") + expand() + -- "o" does not extend the extmark of the active snippet. + feed("Gofn") + expand() + jump(-1) + jump(-1) + -- if linked, should end up back in the original snippet, if not, + -- stay in second. + if link_roots_val == "true" then + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -536,9 +584,11 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} - else - screen:expect{grid=[[ + {2:-- INSERT --} |]], + }) + else + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -568,11 +618,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) + end end - end) + ) end - for _, keep_roots_val in ipairs({"true", "false"}) do + for _, keep_roots_val in ipairs({ "true", "false" }) do it("Root-snippets are stored iff keep_roots=true", function() exec_lua(([[ ls.setup({ @@ -594,7 +646,8 @@ describe("session", function() -- if linked, should end up back in the original snippet, if not, -- stay in second. if keep_roots_val == "true" then - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * ^A{3: short Description} | */ | @@ -624,17 +677,14 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) else - assert( - err:match( - "No Snippet at that position" - ) - ) + assert(err:match("No Snippet at that position")) end end) end - for _, link_children_val in ipairs({"true", "false"}) do + for _, link_children_val in ipairs({ "true", "false" }) do it("Child-snippets are linked iff link_children=true", function() exec_lua(([[ ls.setup({ @@ -650,7 +700,8 @@ describe("session", function() -- expand another child. feed("jjAfn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -680,14 +731,17 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- if linked, should end up back in the original snippet, if not, -- stay in second. if link_children_val == "true" then -- make sure we can jump into the previous child... - jump(-1) jump(-1) - screen:expect{grid=[[ + jump(-1) + jump(-1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -717,10 +771,19 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- ...and from the first child back into the parent... - jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) jump(-1) - screen:expect{grid=[[ + jump(-1) + jump(-1) + jump(-1) + jump(-1) + jump(-1) + jump(-1) + jump(-1) + jump(-1) + screen:expect({ + grid = [[ /** | * ^A{3: short Description} | */ | @@ -750,11 +813,26 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) -- ...and back to the end of the second snippet... -- (first only almost to the end, to make sure we makde the correct number of jumps). - jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -784,9 +862,11 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) jump(1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -816,19 +896,21 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- test inability to jump beyond a few times, I've had bugs -- where after a multiple jumps, a new node became active. jump(1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) jump(1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) jump(1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) -- For good measure, make sure the node is actually still active. jump(-1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -858,9 +940,9 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) else - end end) end @@ -868,12 +950,13 @@ describe("session", function() feed("ifn") expand() -- delete the entier text of a textNode, which will make - -- extmarks_valid() false. + -- extmarks_valid() false. feed("eevllx") -- insert snippet inside the invalid parent. feed("jAfn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -903,11 +986,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- make sure the parent is invalid. jump(-1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -937,11 +1022,12 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- should not move back into the parent. jump(-1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) end) it("region_check_events works correctly", function() exec_lua([[ @@ -962,7 +1048,8 @@ describe("session", function() -- expand snippet. feed("ifn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -992,11 +1079,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- leave its region. feed("Go") -- check we have left the snippet (choiceNode indicator no longer active). - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1026,17 +1115,20 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - |]]} + |]], + }) -- re-activate $0, expand child. - jump(-1) jump(1) + jump(-1) + jump(1) feed("fn") expand() -- jump behind child, activate region_leave, make sure the child and -- root-snippet are _not_ exited. feed("jjAo") - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1066,10 +1158,12 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - |]]} + |]], + }) -- .. and now both are left upon leaving the region of the root-snippet. feed("jji") - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1099,7 +1193,8 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - |]]} + |]], + }) end) it("delete_check_events works correctly", function() exec_lua([[ @@ -1120,7 +1215,8 @@ describe("session", function() -- expand. feed("ifn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1150,11 +1246,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- delete textNode, to trigger unlink_current_if_deleted via esc. feed("eevllx") - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1184,9 +1282,10 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - |]]} + |]], + }) jump(1) - screen:expect{unchanged=true} + screen:expect({ unchanged = true }) end) it("Insertion into non-interactive node works correctly", function() feed("ifn") @@ -1195,7 +1294,8 @@ describe("session", function() -- expand snippet in textNode, ie. s.t. it can't be properly linked up. feed("kifn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | /** | @@ -1225,10 +1325,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- jump into startNode, and back into current node. - jump(-1) jump(-1) - screen:expect{grid=[[ + jump(-1) + jump(-1) + screen:expect({ + grid = [[ /** | * A short Description | /** | @@ -1258,14 +1361,21 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- check jumping out in other direction. feed("jjifn") expand() -- jump to one before jumping out of child-snippet. - jump(1) jump(1) jump(1) jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | /** | @@ -1295,11 +1405,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- leave child. jump(1) -- check back in current node. - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | /** | @@ -1329,7 +1441,8 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) end) it("All operations work as expected in a longer session.", function() exec_lua([[ @@ -1352,7 +1465,8 @@ describe("session", function() expand() feed("kkwwwifn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A /** | * A short Description | @@ -1382,11 +1496,13 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("ggOfn") expand() - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1416,13 +1532,19 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- ensure correct linkage. - jump(1) jump(1) jump(1) - jump(1) jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1452,12 +1574,14 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- enter third choiceNode of second expanded snippet. feed("kkkk$h") exec_lua([[require("luasnip").activate_node()]]) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1487,11 +1611,15 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- check connectivity. - jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1521,18 +1649,21 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- stay at last node. jump(1) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) -- expand in textNode. feed("kkbifn") expand() -- check connectivity. - jump(-1) jump(-1) - screen:expect{grid=[[ + jump(-1) + jump(-1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1562,12 +1693,20 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) -- end up back in last node, not in textNode-expanded snippet. - jump(1) jump(1) jump(1) jump(1) - jump(1) jump(1) jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A short Description | */ | @@ -1597,14 +1736,16 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("gg") exec_lua([[require("luasnip").activate_node()]]) feed("Vjjjjjx") exec_lua("ls.unlink_current_if_deleted()") - screen:expect{grid=[[ + screen:expect({ + grid = [[ ^/** | * A /** | * A short Description | @@ -1634,10 +1775,12 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - 6 fewer lines |]]} + 6 fewer lines |]], + }) -- first snippet is active again. jump(1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * A /** | * A short Description | @@ -1667,15 +1810,26 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- make sure the deleted snippet got disconnected properly. - assert.are.same(exec_lua([[return ls.session.current_nodes[1].parent.snippet.prev.prev and "Node before" or "No node before"]]), "No node before") + assert.are.same( + exec_lua( + [[return ls.session.current_nodes[1].parent.snippet.prev.prev and "Node before" or "No node before"]] + ), + "No node before" + ) -- jump a bit into snippet, so exit_out_of_region changes the current snippet. - jump(1) jump(1) jump(1) jump(1) - jump(1) jump(1) - screen:expect{grid=[[ + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + jump(1) + screen:expect({ + grid = [[ /** | * A /** | * A short Description | @@ -1705,12 +1859,14 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("Go") exec_lua("ls.exit_out_of_region(ls.session.current_nodes[1])") jump(-1) - screen:expect{grid=[[ + screen:expect({ + grid = [[ /** | * ^A{3: /**} | {3: * A short Description} | @@ -1740,6 +1896,7 @@ describe("session", function() {0:~ }| {0:~ }| {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) end) end) diff --git a/tests/integration/snippet_basics_spec.lua b/tests/integration/snippet_basics_spec.lua index d27dacd5b..2510344a9 100644 --- a/tests/integration/snippet_basics_spec.lua +++ b/tests/integration/snippet_basics_spec.lua @@ -250,24 +250,30 @@ describe("snippets_basic", function() }) -- last snippet is not forgotten (yet). exec_lua("ls.jump(1)") - screen:expect{grid=[[ + screen:expect({ + grid = [[ a[^a{3:[]ab}]ab | {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) feed("o") exec_lua("ls.snip_expand(" .. snip .. ")") - screen:expect{grid=[[ + screen:expect({ + grid = [[ a[a[]ab]ab | a[^]ab | - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) exec_lua("ls.jump(-1) ls.jump(-1)") -- first snippet can't be accessed anymore. - screen:expect{grid=[[ + screen:expect({ + grid = [[ a[a[]ab]ab | ^a[]ab | - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) end) it("history=true allows jumping back into exited snippet.", function() @@ -1212,32 +1218,38 @@ describe("snippets_basic", function() feed("iaa") exec_lua([[ ls.expand() ]]) exec_lua([[ ls.jump(1) ]]) - screen:expect{grid=[[ + screen:expect({ + grid = [[ a:(^) | {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("aa") exec_lua([[ ls.expand() ]]) exec_lua([[ ls.jump(1) ]]) - screen:expect{grid=[[ + screen:expect({ + grid = [[ a:(a:(^)) | {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("aa") exec_lua([[ ls.expand() ]]) exec_lua([[ ls.jump(1) ]]) - screen:expect{grid=[[ + screen:expect({ + grid = [[ a:(a:(a:(^))) | {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) -- jump should not move cursor! -- for some reason need multiple jumps to trigger the mistake. exec_lua([[ ls.jump(1)]]) exec_lua([[ ls.jump(1)]]) - screen:expect{unchanged = true} + screen:expect({ unchanged = true }) end) it("exit_out_of_region activates last node of snippet-root.", function() @@ -1254,40 +1266,46 @@ describe("snippets_basic", function() feed("lllliaa") exec_lua("ls.expand()") exec_lua("ls.jump(-1) ls.jump(-1)") - screen:expect{grid=[[ + screen:expect({ + grid = [[ ^( 0-( 0-text )text ) | {0:~ }| - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) feed("o") exec_lua("ls.exit_out_of_region(ls.session.current_nodes[1])") -- verify that we are in the $0 of the nested snippet. exec_lua("ls.jump(-1)") - screen:expect{grid=[[ + screen:expect({ + grid = [[ ( 0-^( 0-text )text ) | | - {2:-- INSERT --} |]]} + {2:-- INSERT --} |]], + }) exec_lua("ls.jump(1)") - screen:expect{grid=[[ + screen:expect({ + grid = [[ ( 0-( ^0{3:-text} )text ) | | - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) end) it("focus correctly adjusts gravities of parent-snippets.", function() - exec_lua[[ + exec_lua([[ ls.setup{ link_children = true } - ]] + ]]) exec_lua([[ls.lsp_expand("a$1$1a")]]) exec_lua([[ls.lsp_expand("b$1")]]) feed("ccc") exec_lua([[ls.active_update_dependents()]]) feed("dddd") -- Here's how this fails if `focus` does not behave correctly (ie. only - -- adjusts extmarks in the snippet the current node is inside): + -- adjusts extmarks in the snippet the current node is inside): -- child has a changed $1, triggers update of own snippets, and -- transitively of the parent-$1. -- Since the parent has a functionNode that copies the $1's text, it @@ -1303,18 +1321,22 @@ describe("snippets_basic", function() -- If, in doing this, the parent-$1-extmark end-gravity is not restored -- to the right, the child-snippet will extend beyond the extmark of -- its parent-node, the parent-$1. - exec_lua[[ls.jump(-1) ls.jump(-1)]] + exec_lua([[ls.jump(-1) ls.jump(-1)]]) -- highlights outer $1. - exec_lua[[ls.jump(1)]] - screen:expect{grid=[[ + exec_lua([[ls.jump(1)]]) + screen:expect({ + grid = [[ a^b{3:cccdddd}bcccdddda | {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) -- and then inner $1. - exec_lua[[ls.jump(1)]] - screen:expect{grid=[[ + exec_lua([[ls.jump(1)]]) + screen:expect({ + grid = [[ ab^c{3:ccdddd}bcccdddda | {0:~ }| - {2:-- SELECT --} |]]} + {2:-- SELECT --} |]], + }) end) end)