Skip to content
Open
6 changes: 6 additions & 0 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 7 additions & 1 deletion doc/luasnip.txt
Original file line number Diff line number Diff line change
@@ -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*
Expand Down Expand Up @@ -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.
Expand Down
168 changes: 168 additions & 0 deletions lua/luasnip/extras/lsp.lua
Original file line number Diff line number Diff line change
@@ -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
40 changes: 21 additions & 19 deletions lua/luasnip/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {}
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 17 additions & 17 deletions lua/luasnip/nodes/snippet.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 {}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}