diff --git a/DOC.md b/DOC.md index 3c15e210a..7143c01ed 100644 --- a/DOC.md +++ b/DOC.md @@ -2851,6 +2851,12 @@ print a short message to the log. return snip.insert_nodes[0] end ``` + - `jumplist_insert_func`: fn(snippet, start_node, end_node, current_node): + Use this callback to change how the snippet in inserted into the jumplist. + `start_node` and `end_node` are the respective nodes of `snippet`, the + snippet which is expanded. + By default, this function: + TODO: describe the default-behavior :( `opts` and any of its parameters may be nil. diff --git a/doc/luasnip.txt b/doc/luasnip.txt index 69ada5a68..8ae54da76 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NVIM v0.8.0 Last change: 2023 January 27 +*luasnip.txt* For NVIM v0.8.0 Last change: 2023 February 02 ============================================================================== Table of Contents *luasnip-table-of-contents* @@ -2799,6 +2799,12 @@ print a short message to the log. `lua function(snip) -- jump_into set the placeholder of the snippet, 1 -- to jump forwards. return snip:jump_into(1)` While this can be used to only insert the snippet `lua function(snip) return snip.insert_nodes[0] end` + - `jumplist_insert_func`: fn(snippet, start_node, end_node, current_node): + Use this callback to change how the snippet in inserted into the jumplist. + `start_node` and `end_node` are the respective nodes of `snippet`, the + snippet which is expanded. + By default, this function: + TODO: describe the default-behavior :( `opts` and any of its parameters may be nil. - `get_active_snip()`: returns the currently active snippet (not node!). - `choice_active()`: true if inside a choiceNode. diff --git a/lua/luasnip/extras/lsp.lua b/lua/luasnip/extras/lsp.lua new file mode 100644 index 000000000..75e3d8091 --- /dev/null +++ b/lua/luasnip/extras/lsp.lua @@ -0,0 +1,168 @@ +local luasnip_ns_id = require("luasnip.session").ns_id +local ls = require("luasnip") +local session = ls.session +local log = require("luasnip.util.log").new("lsp") +local util = require("luasnip.util.util") + +local M = {} + +-- copied from init.lua, maybe find some better way to get it. +local function _jump_into_default(snippet) + 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 + + return util.no_region_check_wrap(snippet.jump_into, snippet, 1) +end + +---Apply text/snippetTextEdits (at most one snippetText though). +---@param snippet_or_text_edits `(snippetTextEdit|textEdit)[]` +--- snippetTextEdit as defined in https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#snippet-textedit) +---@param bufnr number, buffer where the snippet should be expanded. +---@param offset_encoding string|nil, 'utf-8,16,32' or ni +---@param apply_text_edits_fn function, has to apply regular textEdits, most +--- likely `vim.lsp.util.apply_text_edits` (we expect its' interface). +function M.apply_text_edits( + snippet_or_text_edits, + bufnr, + offset_encoding, + apply_text_edits_fn +) + -- plain textEdits, applied using via `apply_text_edits_fn`. + local text_edits = {} + + -- list of snippet-parameters. These contain keys + -- - snippet (parsed snippet) + -- - mark (extmark, textrange replaced by the snippet) + local all_snippet_params = {} + + for _, v in ipairs(snippet_or_text_edits) do + if v.newText and v.insertTextFormat == 2 then + -- from vim.lsp.apply_text_edits. + local start_row = v.range.start.line + local start_col = vim.lsp.util._get_line_byte_from_position( + bufnr, + v.range.start, + offset_encoding + ) + local end_row = v.range["end"].line + local end_col = vim.lsp.util._get_line_byte_from_position( + bufnr, + v.range["end"], + offset_encoding + ) + + table.insert(all_snippet_params, { + snippet_body = v.newText, + mark = vim.api.nvim_buf_set_extmark( + bufnr, + luasnip_ns_id, + start_row, + start_col, + { + end_row = end_row, + end_col = end_col, + } + ), + }) + else + table.insert(text_edits, v) + end + end + + -- first apply regular textEdits... + apply_text_edits_fn(text_edits, bufnr, offset_encoding) + + -- ...then the snippetTextEdits. + + -- store expanded snippets, if there are multiple we need to properly chain them together. + local expanded_snippets = {} + for i, snippet_params in ipairs(all_snippet_params) do + local mark_info = vim.api.nvim_buf_get_extmark_by_id( + bufnr, + luasnip_ns_id, + snippet_params.mark, + { details = true } + ) + local mark_begin_pos = { mark_info[1], mark_info[2] } + local mark_end_pos = { mark_info[3].end_row, mark_info[3].end_col } + + -- luasnip can only expand snippets in the active buffer, so switch (nop if + -- buf already active). + vim.api.nvim_set_current_buf(bufnr) + + -- use expand_opts to chain snippets behind each other and store the + -- expanded snippets. + -- With the regular expand_opts, we will immediately jump into the + -- first snippet, if it contains an i(1), the following snippets will + -- belong inside it, which we don't want here: we want the i(0) of a + -- snippet to lead to the next (also skipping the i(-1)). + -- Even worse: by default, we would jump into the snippets during + -- snip_expand, which should only happen for the first, the later + -- snippets should be reached by jumping through the previous ones. + local expand_opts = { + pos = mark_begin_pos, + clear_region = { + from = mark_begin_pos, + to = mark_end_pos, + }, + } + + if i == 1 then + -- for first snippet: jump into it, and store the expanded snippet. + expand_opts.jump_into_func = function(snip) + expanded_snippets[i] = snip + local cr = _jump_into_default(snip) + return cr + end + else + -- don't jump into the snippet, just store it. + expand_opts.jump_into_func = function(snip) + expanded_snippets[i] = snip + + -- let the already-active node stay active. + return session.current_nodes[bufnr] + end + -- jump from previous i0 directly to start_node. + expand_opts.jumplist_insert_func = function(_, start_node, _, _) + start_node.prev = expanded_snippets[i - 1].insert_nodes[0] + expanded_snippets[i - 1].insert_nodes[0].next = start_node + + -- skip start_node while jumping around. + -- start_node of first snippet behaves normally! + function start_node:jump_into(dir, no_move) + return (dir == 1 and self.next or self.prev):jump_into( + dir, + no_move + ) + end + end + end + + ls.lsp_expand(snippet_params.snippet_body, expand_opts) + end +end + +function M.update_capabilities(capabilities) + if not capabilities.experimental then + capabilities.experimental = {} + end + capabilities.experimental.snippetTextEdit = true + + return capabilities +end + +return M diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index d8d01fac9..1ebac9d3b 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -185,6 +185,23 @@ local function locally_jumpable(dir) end local function _jump_into_default(snippet) + 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 + return util.no_region_check_wrap(snippet.jump_into, snippet, 1) end @@ -197,6 +214,8 @@ local function snip_expand(snippet, opts) -- override with current position if none given. opts.pos = opts.pos or util.get_cursor_0ind() opts.jump_into_func = opts.jump_into_func or _jump_into_default + opts.jumplist_insert_func = opts.jumplist_insert_func + or require("luasnip.nodes.snippet").default_jumplist_insert snip.trigger = opts.expand_params.trigger or snip.trigger snip.captures = opts.expand_params.captures or {} @@ -233,27 +252,10 @@ local function snip_expand(snippet, opts) snip:trigger_expand( session.current_nodes[vim.api.nvim_get_current_buf()], pos_id, - env + env, + opts.jumplist_insert_func ) - 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) diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index d46a39105..61ebdb70c 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -385,7 +385,7 @@ function Snippet:remove_from_jumplist() end end -local function insert_into_jumplist(snippet, start_node, current_node) +local function insert_into_jumplist(snippet, start_node, end_node, current_node) if current_node then -- currently at the endpoint (i(0)) of another snippet, this snippet -- is inserted _behind_ that snippet. @@ -394,13 +394,13 @@ local function insert_into_jumplist(snippet, start_node, current_node) 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] + current_node.next.prev = end_node else -- next is outer insertNode. - current_node.next.inner_last = snippet.insert_nodes[0] + current_node.next.inner_last = end_node end end - snippet.insert_nodes[0].next = current_node.next + end_node.next = current_node.next current_node.next = start_node start_node.prev = current_node elseif current_node.pos == -1 then @@ -411,27 +411,20 @@ local function insert_into_jumplist(snippet, start_node, current_node) current_node.prev.inner_first = snippet end end - snippet.insert_nodes[0].next = current_node + end_node.next = current_node start_node.prev = current_node.prev - current_node.prev = snippet.insert_nodes[0] + current_node.prev = end_node else - snippet.insert_nodes[0].next = current_node + end_node.next = current_node -- jump into snippet directly. current_node.inner_first = snippet - current_node.inner_last = snippet.insert_nodes[0] + current_node.inner_last = end_node start_node.prev = current_node 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 end -function Snippet:trigger_expand(current_node, pos_id, env) +function Snippet:trigger_expand(current_node, pos_id, env, jumplist_insert_func) local pos = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, pos_id, {}) local pre_expand_res = self:event(events.pre_expand, { expand_pos = pos }) or {} @@ -509,7 +502,13 @@ function Snippet:trigger_expand(current_node, pos_id, env) start_node.pos = -1 start_node.parent = self - insert_into_jumplist(self, start_node, current_node) + -- snippet is between i(-1)(startNode) and i(0). + self.insert_nodes[0].prev = self + start_node.next = self + self.next = self.insert_nodes[0] + self.prev = start_node + + jumplist_insert_func(self, start_node, self.insert_nodes[0], current_node) end -- returns copy of snip if it matches, nil if not. @@ -1205,4 +1204,5 @@ return { wrap_nodes_in_snippetNode = wrap_nodes_in_snippetNode, init_snippet_context = init_snippet_context, init_snippet_opts = init_snippet_opts, + default_jumplist_insert = insert_into_jumplist, }