Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interface for applying snippetTextEdits #577

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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,
}