-
-
Notifications
You must be signed in to change notification settings - Fork 245
Misc
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:
If you aren't using VimTeX or want to be independent from its in_mathzone()
function, then you might find the below suggestion helpful. You can also hook it to your snippets for markdown
as well, not just for .tex
.
-- This module contains functions to check if the cursor is in a particular environment
local M = {}
-- Get the current line number and column
local function get_cursor_pos()
local cursor = vim.api.nvim_win_get_cursor(0)
-- vim API returns 0-based column, so we need to add 1 to column
return cursor[1], cursor[2] + 1
end
-- Get the range of visible rows
local function get_visible_rows(current_row)
-- Get the height of the current window
local win_height = vim.api.nvim_win_get_height(0)
-- Calculate the first and last visible rows
local first_visible_row = math.max(1, current_row - math.floor(win_height / 2))
local last_visible_row = math.min(vim.api.nvim_buf_line_count(0), current_row + math.ceil(win_height / 2))
return first_visible_row, last_visible_row
end
-- Search for start and end patterns around the cursor
local function search_pair(env_name, current_row)
-- Get the first and last visible rows
local first_visible_row, last_visible_row = get_visible_rows(current_row)
-- Define the start and end patterns
local start_patterns = {
"\\begin{" .. env_name .. "}",
"\\start" .. env_name
}
local end_patterns = {
"\\end{" .. env_name .. "}",
"\\stop" .. env_name
}
local start_pos, end_pos = nil, nil
-- Function to search a list of patterns in a specified range and direction
local function search_patterns(patterns, from, to, step)
for line_num = from, to, step do
local line = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1]
for _, pattern in ipairs(patterns) do
if line:find(pattern) then
return line_num
end
end
end
return nil
end
-- Search upwards for the start pattern in the visible range
start_pos = search_patterns(start_patterns, current_row, first_visible_row, -1)
-- If the start pattern not found in the visible range, then search further up
if not start_pos then
start_pos = search_patterns(start_patterns, current_row, 1, -1)
end
-- Search downwards for the end pattern in the visible range
end_pos = search_patterns(end_patterns, current_row, last_visible_row, 1)
-- If the end pattern not found, then search further down
if not end_pos then
end_pos = search_patterns(end_patterns, current_row, vim.api.nvim_buf_line_count(0), 1)
end
return start_pos, end_pos
end
-- Check if the cursor is inside a given environment
local function in_env(env_name)
local current_row, _ = get_cursor_pos()
local start_pos, end_pos = search_pair(env_name, current_row)
if start_pos and end_pos then
return current_row >= start_pos and current_row <= end_pos
end
return false
end
-- Function to identify math zones in a line
local function find_math_zones(line)
local math_patterns = {
"%$%$.+%$%$", -- $$...$$
"\\\\%[.+\\\\%]", -- \[...\]
"\\\\%(.+\\\\%)", -- \( ... \)
"%$[^$]+%$", -- inline math $...$
}
local math_zones = {}
for _, pattern in ipairs(math_patterns) do
local s, e = 1, 1
while s do
s, e = string.find(line, pattern, e)
if s then
table.insert(math_zones, {start = s, stop = e})
e = e + 1
end
end
end
return math_zones -- = { { start = 1, stop = 16 }, { start = 18, stop = 33 }, ... }
end
-- Check if the cursor is inside a math zone
function M.is_mathzone()
-- Get the cursor position
local current_row, current_col = get_cursor_pos()
-- Get the content of the current line
local line = vim.api.nvim_buf_get_lines(0, current_row - 1, current_row, false)[1]
-- Find the math zones in the line
local math_zones = find_math_zones(line)
for _, zone in ipairs(math_zones) do
-- Check if the cursor is inside a math zone
if current_col >= zone.start and current_col <= zone.stop then
return true
end
end
return false
end
-- Specific environment checks
M.in_text = function() return in_env("text") and not M.math_mode() end
M.in_math_range = function() return in_env("formula") end
M.in_tikz = function() return in_env("tikzpicture") end
M.in_bullets = function() return in_env("itemize") or in_env("enumerate") end
M.in_MPcode = function() return in_env("MPcode") end
M.math_mode = function() return M.is_mathzone() or M.in_math_range() end
-- Function to apply settings to math zones
-- Similar settings can be done for math ranges as well, but perhaps they might cause some distractions.
local function apply_math_zone_settings()
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
for i, line in ipairs(lines) do
local math_zones = find_math_zones(line)
for _, zone in ipairs(math_zones) do
-- Apply specific settings or behaviours to the identified math zones
vim.api.nvim_buf_add_highlight(0, -1, 'MathZone', i - 1, zone.start - 1, zone.stop)
end
end
end
-- Command to apply math zone settings
vim.api.nvim_create_user_command('ApplyMathZoneSettings', apply_math_zone_settings, {})
-- Highlight math zones with bright red
vim.cmd [[highlight MathZone guifg=#ff0000 gui=bold]]
-- Autocmd to apply math zone settings
vim.api.nvim_create_autocmd({"BufReadPost", "BufWritePost"}, {
pattern = "*.tex",
command = "ApplyMathZoneSettings"
})
-- Command to check if the cursor is in a math zone
-- vim.api.nvim_create_user_command('CheckCursorMathZone', function()
-- if M.is_mathzone() then
-- print("Cursor is in a math zone")
-- else
-- print("Cursor is NOT in a math zone")
-- end
-- end, {})
return M
Next, test it with:
-- Assumming that you have a directory called `TEX` containing `conditions.lua` under the `lua` one.
local tex = require("TEX.conditions")
autosnippet({ trig='//', name='fraction', dscr="fraction (general)"},
fmta([[
\frac{<>}{<>} <>
]],
{ i(1), i(2), i(0) }),
{ condition = tex.math_mode }),
And in the following
\begin{document} or \starttext
\startformula or \begin{formula}
{1}
\stopformula or \end{formula}
{2}
${3}$ {4} ${5}$
\end{document} or \stoptext
the trigger //
should be expanded to \frac{}{}
at {1}, {3} and {5} but not at {2} or {4}.