Misc - L3MON4D3/LuaSnip GitHub Wiki
local current_nsid = vim.api.nvim_create_namespace("LuaSnipChoiceListSelections")
local current_win = nil
local function window_for_choiceNode(choiceNode)
local buf = vim.api.nvim_create_buf(false, true)
local buf_text = {}
local row_selection = 0
local row_offset = 0
local text
for _, node in ipairs(choiceNode.choices) do
text = node:get_docstring()
-- find one that is currently showing
if node == choiceNode.active_choice then
-- current line is starter from buffer list which is length usually
row_selection = #buf_text
-- finding how many lines total within a choice selection
row_offset = #text
end
vim.list_extend(buf_text, text)
end
vim.api.nvim_buf_set_text(buf, 0,0,0,0, buf_text)
local w, h = vim.lsp.util._make_floating_popup_size(buf_text)
-- adding highlight so we can see which one is been selected.
local extmark = vim.api.nvim_buf_set_extmark(buf,current_nsid,row_selection ,0,
{hl_group = 'incsearch',end_line = row_selection + row_offset})
-- shows window at a beginning of choiceNode.
local win = vim.api.nvim_open_win(buf, false, {
relative = "win", width = w, height = h, bufpos = choiceNode.mark:pos_begin_end(), style = "minimal", border = 'rounded'})
-- return with 3 main important so we can use them again
return {win_id = win,extmark = extmark,buf = buf}
end
function choice_popup(choiceNode)
-- build stack for nested choiceNodes.
if current_win then
vim.api.nvim_win_close(current_win.win_id, true)
vim.api.nvim_buf_del_extmark(current_win.buf,current_nsid,current_win.extmark)
end
local create_win = window_for_choiceNode(choiceNode)
current_win = {
win_id = create_win.win_id,
prev = current_win,
node = choiceNode,
extmark = create_win.extmark,
buf = create_win.buf
}
end
function update_choice_popup(choiceNode)
vim.api.nvim_win_close(current_win.win_id, true)
vim.api.nvim_buf_del_extmark(current_win.buf,current_nsid,current_win.extmark)
local create_win = window_for_choiceNode(choiceNode)
current_win.win_id = create_win.win_id
current_win.extmark = create_win.extmark
current_win.buf = create_win.buf
end
function choice_popup_close()
vim.api.nvim_win_close(current_win.win_id, true)
vim.api.nvim_buf_del_extmark(current_win.buf,current_nsid,current_win.extmark)
-- now we are checking if we still have previous choice we were in after exit nested choice
current_win = current_win.prev
if current_win then
-- reopen window further down in the stack.
local create_win = window_for_choiceNode(current_win.node)
current_win.win_id = create_win.win_id
current_win.extmark = create_win.extmark
current_win.buf = create_win.buf
end
end
vim.cmd([[
augroup choice_popup
au!
au User LuasnipChoiceNodeEnter lua choice_popup(require("luasnip").session.event_node)
au User LuasnipChoiceNodeLeave lua choice_popup_close()
au User LuasnipChangeChoice lua update_choice_popup(require("luasnip").session.event_node)
augroup END
]])
This makes use of the nodeEnter/Leave/ChangeChoice events to show available choices. A similar effect can also be achieved by overriding the vim.ui.select
-menu and binding select_choice
to a key.
This can be useful if a language server returns snippets that suffer from the limitations of
lsp-snippets. In this particular case clangd
s snippets for member initializatizer lists always
look like this: m_SomeMember(${0:m_SomeMembersType})
. This is suboptimal in some ways, mainly
that
- Only normal parenthesis are possible, whereas curly braces may be preferred.
- Luasnip treats the $0-placeholder as a one-time-stop, meaning that once SELECT is exited, there's no way to change it.
To fix this, we need to
- intercept and change the snippet received via LSP.
- override the expansion-function (here in
nvim-cmp
) to use our new snippet, if available.
We override the client.request
-function so all responses from LSP go through our function.
There we override the handler for "textDocument/completion"
to modify the TextEdit
returned by the language server.
("Inspired" by this comment).
local ls = require("luasnip")
local s = ls.snippet
local r = ls.restore_node
local i = ls.insert_node
local t = ls.text_node
local c = ls.choice_node
lspsnips = {}
nvim_lsp.clangd.setup{
on_attach = function(client)
local orig_rpc_request = client.rpc.request
function client.rpc.request(method, params, handler, ...)
local orig_handler = handler
if method == 'textDocument/completion' then
-- Idiotic take on <https://github.com/fannheyward/coc-pyright/blob/6a091180a076ec80b23d5fc46e4bc27d4e6b59fb/src/index.ts#L90-L107>.
handler = function(...)
local err, result = ...
if not err and result then
local items = result.items or result
for _, item in ipairs(items) do
-- override snippets for kind `field`, matching the snippets for member initializer lists.
if item.kind == vim.lsp.protocol.CompletionItemKind.Field and
item.textEdit.newText:match("^[%w_]+%(${%d+:[%w_]+}%)$") then
local snip_text = item.textEdit.newText
local name = snip_text:match("^[%w_]+")
local type = snip_text:match("%{%d+:([%w_]+)%}")
-- the snippet is stored in a separate table. It is not stored in the `item` passed to
-- cmp, because it will be copied there and cmps [copy](https://github.com/hrsh7th/nvim-cmp/blob/ac476e05df2aab9f64cdd70b6eca0300785bb35d/lua/cmp/utils/misc.lua#L125-L143) doesn't account
-- for self-referential tables and metatables (rightfully so, a response from lsp
-- would contain neither), both of which are vital for a snippet.
lspsnips[snip_text] = s("", {
t(name),
c(1, {
-- use a restoreNode to remember the text typed here.
{t"(", r(1, "type", i(1, type)), t")"},
{t"{", r(1, "type"), t"}"},
}, {restore_cursor = true})
})
end
end
end
return orig_handler(...)
end
end
return orig_rpc_request(method, params, handler, ...)
end
end
}
The last missing piece is changing the "default"
snippet-expansion-function in cmp to account for our snippet:
cmp.setup {
snippet = {
expand = function(args)
-- check if we created a snippet for this lsp-snippet.
if lspsnips[args.body] then
-- use `snip_expand` to expand the snippet at the cursor position.
require("luasnip").snip_expand(lspsnips[args.body])
else
require("luasnip").lsp_expand(args.body)
end
end,
},
}
et voilà:
Normally, dynamicNodes can only update when text inside the snippet changed. This is pretty powerful, but not enough
for eg. a latex-table-snippet, where the number of rows should be adjustable on-the-fly (otherwise a regex-triggered snippet with
trig=tab(%d+)x(%d+)
would suffice).
This isn't possible OOTB, so we need to write a function that
- Runs some other function whose output will be used in the dynamicNode-function.
- Updates the dynamicNode.
and then call that function using a mapping (optional, but much more comfortable than calling it manually).
local ls = require("luasnip")
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local function find_dynamic_node(node)
-- the dynamicNode-key is set on snippets generated by a dynamicNode only (its'
-- actual use is to refer to the dynamicNode that generated the snippet).
while not node.dynamicNode do
node = node.parent
end
return node.dynamicNode
end
local external_update_id = 0
-- func_indx to update the dynamicNode with different functions.
function dynamic_node_external_update(func_indx)
-- most of this function is about restoring the cursor to the correct
-- position+mode, the important part are the few lines from
-- `dynamic_node.snip:store()`.
-- find current node and the innermost dynamicNode it is inside.
local current_node = ls.session.current_nodes[vim.api.nvim_get_current_buf()]
local dynamic_node = find_dynamic_node(current_node)
-- to identify current node in new snippet, if it is available.
external_update_id = external_update_id + 1
current_node.external_update_id = external_update_id
local current_node_key = current_node.key
-- store which mode we're in to restore later.
local insert_pre_call = vim.fn.mode() == "i"
-- is byte-indexed! Doesn't matter here, but important to be aware of.
local cursor_pos_end_relative = util.pos_sub(
util.get_cursor_0ind(),
current_node.mark:get_endpoint(1)
)
-- leave current generated snippet.
node_util.leave_nodes_between(dynamic_node.snip, current_node)
-- call update-function.
local func = dynamic_node.user_args[func_indx]
if func then
-- the same snippet passed to the dynamicNode-function. Any output from func
-- should be stored in it under some unused key.
func(dynamic_node.parent.snippet)
end
-- last_args is used to store the last args that were used to generate the
-- snippet. If this function is called, these will most probably not have
-- changed, so they are set to nil, which will force an update.
dynamic_node.last_args = nil
dynamic_node:update()
-- everything below here isn't strictly necessary, but it's pretty nice to have.
-- try to find the node we marked earlier, or a node with the same key.
-- Both are getting equal priority here, it might make sense to give "exact
-- same node" higher priority by doing two searches (but that would require
-- two searches :( )
local target_node = dynamic_node:find_node(function(test_node)
return (test_node.external_update_id == external_update_id) or (current_node_key ~= nil and test_node.key == current_node_key)
end)
if target_node then
-- the node that the cursor was in when changeChoice was called exists
-- in the active choice! Enter it and all nodes between it and this choiceNode,
-- then set the cursor.
node_util.enter_nodes_between(dynamic_node, target_node)
if insert_pre_call then
-- restore cursor-position if the node, or a corresponding node,
-- could be found.
-- It is restored relative to the end of the node (as opposed to the
-- beginning). This does not matter if the text in the node is
-- unchanged, but if the length changed, we may move the cursor
-- relative to its immediate neighboring characters.
-- I assume that it is more likely that the text before the cursor
-- got longer (since it is very likely that the cursor is just at
-- the end of the node), and thus restoring relative to the
-- beginning would shift the cursor back.
--
-- However, restoring to any fixed endpoint is likely to not be
-- perfect, an interesting enhancement would be to compare the new
-- and old text/[neighborhood of the cursor], and find its new position
-- based on that.
util.set_cursor_0ind(
util.pos_add(
target_node.mark:get_endpoint(1),
cursor_pos_end_relative
)
)
else
node_util.select_node(target_node)
end
-- set the new current node correctly.
ls.session.current_nodes[vim.api.nvim_get_current_buf()] = target_node
else
-- the marked node wasn't found, just jump into the new snippet noremally.
ls.session.current_nodes[vim.api.nvim_get_current_buf()] = dynamic_node.snip:jump_into(1)
end
end
Bind the function to some key:
vim.api.nvim_set_keymap('i', "<C-t>", '<cmd>lua _G.dynamic_node_external_update(1)<Cr>', {noremap = true})
vim.api.nvim_set_keymap('s', "<C-t>", '<cmd>lua _G.dynamic_node_external_update(1)<Cr>', {noremap = true})
vim.api.nvim_set_keymap('i', "<C-g>", '<cmd>lua _G.dynamic_node_external_update(2)<Cr>', {noremap = true})
vim.api.nvim_set_keymap('s', "<C-g>", '<cmd>lua _G.dynamic_node_external_update(2)<Cr>', {noremap = true})
It may be useful to bind even more numbers (3-???????), but two suffice for this example.
Now it's time to make use of the new function:
local function column_count_from_string(descr)
-- this won't work for all cases, but it's simple to improve
-- (feel free to do so! :D )
return #(descr:gsub("[^clm]", ""))
end
-- function for the dynamicNode.
local tab = function(args, snip)
local cols = column_count_from_string(args[1][1])
-- snip.rows will not be set by default, so handle that case.
-- it's also the value set by the functions called from dynamic_node_external_update().
if not snip.rows then
snip.rows = 1
end
local nodes = {}
-- keep track of which insert-index we're at.
local ins_indx = 1
for j = 1, snip.rows do
-- use restoreNode to not lose content when updating.
table.insert(nodes, r(ins_indx, tostring(j).."x1", i(1)))
ins_indx = ins_indx+1
for k = 2, cols do
table.insert(nodes, t" & ")
table.insert(nodes, r(ins_indx, tostring(j).."x"..tostring(k), i(1)))
ins_indx = ins_indx+1
end
table.insert(nodes, t{"\\\\", ""})
end
-- fix last node.
nodes[#nodes] = t""
return sn(nil, nodes)
end
s("tab", fmt([[
\begin{{tabular}}{{{}}}
{}
\end{{tabular}}
]], {i(1, "c"), d(2, tab, {1}, {
user_args = {
-- Pass the functions used to manually update the dynamicNode as user args.
-- The n-th of these functions will be called by dynamic_node_external_update(n).
-- These functions are pretty simple, there's probably some cool stuff one could do
-- with `ui.input`
function(snip) snip.rows = snip.rows + 1 end,
-- don't drop below one.
function(snip) snip.rows = math.max(snip.rows - 1, 1) end
}
} )}))
<C-t>
, now calls the first function, increasing the number of rows, whereas <C-g>
calls the second function, decreasing it.
And here's the result: