feat(lsp): support for choice snippet nodes

This commit is contained in:
Maria José Solano
2023-10-26 22:53:38 -07:00
committed by Mathias Fußenegger
parent ad867fee26
commit 7e36c8e972
2 changed files with 76 additions and 23 deletions

View File

@ -104,8 +104,9 @@ end
--- @class vim.snippet.Tabstop --- @class vim.snippet.Tabstop
--- @field extmark_id integer --- @field extmark_id integer
--- @field index integer
--- @field bufnr integer --- @field bufnr integer
--- @field index integer
--- @field choices? string[]
local Tabstop = {} local Tabstop = {}
--- Creates a new tabstop. --- Creates a new tabstop.
@ -114,8 +115,9 @@ local Tabstop = {}
--- @param index integer --- @param index integer
--- @param bufnr integer --- @param bufnr integer
--- @param range Range4 --- @param range Range4
--- @param choices? string[]
--- @return vim.snippet.Tabstop --- @return vim.snippet.Tabstop
function Tabstop.new(index, bufnr, range) function Tabstop.new(index, bufnr, range, choices)
local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], { local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], {
right_gravity = false, right_gravity = false,
end_right_gravity = true, end_right_gravity = true,
@ -125,7 +127,7 @@ function Tabstop.new(index, bufnr, range)
}) })
local self = setmetatable( local self = setmetatable(
{ index = index, bufnr = bufnr, extmark_id = extmark_id }, { extmark_id = extmark_id, bufnr = bufnr, index = index, choices = choices },
{ __index = Tabstop } { __index = Tabstop }
) )
@ -173,9 +175,9 @@ local Session = {}
--- @package --- @package
--- @param bufnr integer --- @param bufnr integer
--- @param snippet_extmark integer --- @param snippet_extmark integer
--- @param tabstop_ranges table<integer, Range4[]> --- @param tabstop_data table<integer, { range: Range4, choices?: string[] }[]>
--- @return vim.snippet.Session --- @return vim.snippet.Session
function Session.new(bufnr, snippet_extmark, tabstop_ranges) function Session.new(bufnr, snippet_extmark, tabstop_data)
local self = setmetatable({ local self = setmetatable({
bufnr = bufnr, bufnr = bufnr,
extmark_id = snippet_extmark, extmark_id = snippet_extmark,
@ -184,10 +186,10 @@ function Session.new(bufnr, snippet_extmark, tabstop_ranges)
}, { __index = Session }) }, { __index = Session })
-- Create the tabstops. -- Create the tabstops.
for index, ranges in pairs(tabstop_ranges) do for index, ranges in pairs(tabstop_data) do
for _, range in ipairs(ranges) do for _, data in ipairs(ranges) do
self.tabstops[index] = self.tabstops[index] or {} self.tabstops[index] = self.tabstops[index] or {}
table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, range)) table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, data.range, data.choices))
end end
end end
@ -222,6 +224,22 @@ end
--- @field private _session? vim.snippet.Session --- @field private _session? vim.snippet.Session
local M = { session = nil } local M = { session = nil }
--- Displays the choices for the given tabstop as completion items.
---
--- @param tabstop vim.snippet.Tabstop
local function display_choices(tabstop)
assert(tabstop.choices, 'Tabstop has no choices')
local start_col = tabstop:get_range()[2] + 1
local matches = vim.iter.map(function(choice)
return { word = choice }
end, tabstop.choices)
vim.defer_fn(function()
vim.fn.complete(start_col, matches)
end, 100)
end
--- Select the given tabstop range. --- Select the given tabstop range.
--- ---
--- @param tabstop vim.snippet.Tabstop --- @param tabstop vim.snippet.Tabstop
@ -246,17 +264,25 @@ local function select_tabstop(tabstop)
local range = tabstop:get_range() local range = tabstop:get_range()
local mode = vim.fn.mode() local mode = vim.fn.mode()
if vim.fn.pumvisible() ~= 0 then
-- Close the choice completion menu if open.
vim.fn.complete(vim.fn.col('.'), {})
end
-- Move the cursor to the start of the tabstop. -- Move the cursor to the start of the tabstop.
vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] }) vim.api.nvim_win_set_cursor(0, { range[1] + 1, range[2] })
-- For empty and the final tabstop, start insert mode at the end of the range. -- For empty, choice and the final tabstops, start insert mode at the end of the range.
if tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then if tabstop.choices or tabstop.index == 0 or (range[1] == range[3] and range[2] == range[4]) then
if mode ~= 'i' then if mode ~= 'i' then
if mode == 's' then if mode == 's' then
feedkeys('<Esc>') feedkeys('<Esc>')
end end
vim.cmd.startinsert({ bang = range[4] >= #vim.api.nvim_get_current_line() }) vim.cmd.startinsert({ bang = range[4] >= #vim.api.nvim_get_current_line() })
end end
if tabstop.choices then
display_choices(tabstop)
end
else else
-- Else, select the tabstop's text. -- Else, select the tabstop's text.
if mode ~= 'n' then if mode ~= 'n' then
@ -297,7 +323,6 @@ local function setup_autocmds(bufnr)
return true return true
end end
-- Update the current tabstop to be the one containing the cursor.
for tabstop_index, tabstops in pairs(M._session.tabstops) do for tabstop_index, tabstops in pairs(M._session.tabstops) do
for _, tabstop in ipairs(tabstops) do for _, tabstop in ipairs(tabstops) do
local range = tabstop:get_range() local range = tabstop:get_range()
@ -305,7 +330,6 @@ local function setup_autocmds(bufnr)
(cursor_row > range[1] or (cursor_row == range[1] and cursor_col >= range[2])) (cursor_row > range[1] or (cursor_row == range[1] and cursor_col >= range[2]))
and (cursor_row < range[3] or (cursor_row == range[3] and cursor_col <= range[4])) and (cursor_row < range[3] or (cursor_row == range[3] and cursor_col <= range[4]))
then then
M._session.current_tabstop = tabstop
if tabstop_index ~= 0 then if tabstop_index ~= 0 then
return return
end end
@ -377,14 +401,16 @@ function M.expand(input)
end end
-- Keep track of tabstop nodes during expansion. -- Keep track of tabstop nodes during expansion.
--- @type table<integer, Range4[]> --- @type table<integer, { range: Range4, choices?: string[] }[]>
local tabstop_ranges = {} local tabstop_data = {}
--- @param index integer --- @param index integer
--- @param placeholder string? --- @param placeholder? string
local function add_tabstop(index, placeholder) --- @param choices? string[]
tabstop_ranges[index] = tabstop_ranges[index] or {} local function add_tabstop(index, placeholder, choices)
table.insert(tabstop_ranges[index], compute_tabstop_range(snippet_text, placeholder)) tabstop_data[index] = tabstop_data[index] or {}
local range = compute_tabstop_range(snippet_text, placeholder)
table.insert(tabstop_data[index], { range = range, choices = choices })
end end
--- Appends the given text to the snippet, taking care of indentation. --- Appends the given text to the snippet, taking care of indentation.
@ -428,7 +454,7 @@ function M.expand(input)
append_to_snippet(value) append_to_snippet(value)
elseif type == G.NodeType.Choice then elseif type == G.NodeType.Choice then
--- @cast data vim.snippet.ChoiceData --- @cast data vim.snippet.ChoiceData
append_to_snippet(data.values[1]) add_tabstop(data.tabstop, nil, data.values)
elseif type == G.NodeType.Variable then elseif type == G.NodeType.Variable then
--- @cast data vim.snippet.VariableData --- @cast data vim.snippet.VariableData
-- Try to get the variable's value. -- Try to get the variable's value.
@ -436,7 +462,7 @@ function M.expand(input)
if not value then if not value then
-- Unknown variable, make this a tabstop and use the variable name as a placeholder. -- Unknown variable, make this a tabstop and use the variable name as a placeholder.
value = data.name value = data.name
local tabstop_indexes = vim.tbl_keys(tabstop_ranges) local tabstop_indexes = vim.tbl_keys(tabstop_data)
local index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes)) + 1 local index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes)) + 1
add_tabstop(index, value) add_tabstop(index, value)
end end
@ -449,8 +475,8 @@ function M.expand(input)
-- $0, which defaults to the end of the snippet, defines the final cursor position. -- $0, which defaults to the end of the snippet, defines the final cursor position.
-- Make sure the snippet has exactly one of these. -- Make sure the snippet has exactly one of these.
if vim.tbl_contains(vim.tbl_keys(tabstop_ranges), 0) then if vim.tbl_contains(vim.tbl_keys(tabstop_data), 0) then
assert(#tabstop_ranges[0] == 1, 'Snippet has multiple $0 tabstops') assert(#tabstop_data[0] == 1, 'Snippet has multiple $0 tabstops')
else else
add_tabstop(0) add_tabstop(0)
end end
@ -469,7 +495,7 @@ function M.expand(input)
right_gravity = false, right_gravity = false,
end_right_gravity = true, end_right_gravity = true,
}) })
M._session = Session.new(bufnr, snippet_extmark, tabstop_ranges) M._session = Session.new(bufnr, snippet_extmark, tabstop_data)
-- Jump to the first tabstop. -- Jump to the first tabstop.
M.jump(1) M.jump(1)

View File

@ -6,6 +6,7 @@ local exec_lua = helpers.exec_lua
local feed = helpers.feed local feed = helpers.feed
local matches = helpers.matches local matches = helpers.matches
local pcall_err = helpers.pcall_err local pcall_err = helpers.pcall_err
local sleep = helpers.sleep
describe('vim.snippet', function() describe('vim.snippet', function()
before_each(function() before_each(function()
@ -171,4 +172,30 @@ describe('vim.snippet', function()
feed('<esc>O-- A comment') feed('<esc>O-- A comment')
eq(false, exec_lua('return vim.snippet.active()')) eq(false, exec_lua('return vim.snippet.active()'))
end) end)
it('inserts choice', function ()
test_success({ 'console.${1|assert,log,error|}()' }, { 'console.()' })
sleep(100)
feed('<Down><C-y>')
eq({ 'console.log()' }, helpers.buf_lines(0))
end)
it('closes the choice completion menu when jumping', function ()
test_success({ 'console.${1|assert,log,error|}($2)' }, { 'console.()' })
sleep(100)
exec_lua('vim.snippet.jump(1)')
eq(0, exec_lua('return vim.fn.pumvisible()'))
end)
it('jumps to next tabstop after inserting choice', function()
test_success(
{ '${1|public,protected,private|} function ${2:name}() {', '\t$0', '}' },
{ ' function name() {', '\t', '}' }
)
sleep(100)
feed('<C-y><Tab>')
sleep(10)
feed('foo')
eq({ 'public function foo() {', '\t', '}' }, helpers.buf_lines(0))
end)
end) end)