mirror of
https://github.com/neovim/neovim.git
synced 2025-09-05 19:08:15 +00:00
fix(snippet): adjacent tabstops without placeholders (#35167)
* fix(snippet): adjacent tabstops without placeholders * test(snippet): add tests for directly adjacent tabstops
This commit is contained in:
@@ -105,6 +105,7 @@ end
|
||||
--- @field extmark_id integer
|
||||
--- @field bufnr integer
|
||||
--- @field index integer
|
||||
--- @field placement integer
|
||||
--- @field choices? string[]
|
||||
local Tabstop = {}
|
||||
|
||||
@@ -113,10 +114,11 @@ local Tabstop = {}
|
||||
--- @package
|
||||
--- @param index integer
|
||||
--- @param bufnr integer
|
||||
--- @param placement integer
|
||||
--- @param range Range4
|
||||
--- @param choices? string[]
|
||||
--- @return vim.snippet.Tabstop
|
||||
function Tabstop.new(index, bufnr, range, choices)
|
||||
function Tabstop.new(index, bufnr, placement, range, choices)
|
||||
local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, snippet_ns, range[1], range[2], {
|
||||
right_gravity = true,
|
||||
end_right_gravity = false,
|
||||
@@ -125,10 +127,13 @@ function Tabstop.new(index, bufnr, range, choices)
|
||||
hl_group = hl_group,
|
||||
})
|
||||
|
||||
local self = setmetatable(
|
||||
{ extmark_id = extmark_id, bufnr = bufnr, index = index, choices = choices },
|
||||
{ __index = Tabstop }
|
||||
)
|
||||
local self = setmetatable({
|
||||
extmark_id = extmark_id,
|
||||
bufnr = bufnr,
|
||||
index = index,
|
||||
placement = placement,
|
||||
choices = choices,
|
||||
}, { __index = Tabstop })
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -162,15 +167,31 @@ function Tabstop:set_text(text)
|
||||
vim.api.nvim_buf_set_text(self.bufnr, range[1], range[2], range[3], range[4], text_to_lines(text))
|
||||
end
|
||||
|
||||
---@alias (private) vim.snippet.TabStopGravity
|
||||
--- | "expand" Expand the (usually current) tabstop on text insert
|
||||
--- | "lock" The tabstop should NOT move on text insert
|
||||
--- | "shift" The tabstop should move on text insert (default)
|
||||
|
||||
--- Sets the right gravity of the tabstop's extmark.
|
||||
---
|
||||
--- @package
|
||||
--- @param right_gravity boolean
|
||||
function Tabstop:set_right_gravity(right_gravity)
|
||||
---@package
|
||||
---@param target vim.snippet.TabStopGravity
|
||||
function Tabstop:set_gravity(target)
|
||||
local right_gravity = true
|
||||
local end_right_gravity = true
|
||||
|
||||
if target == 'expand' then
|
||||
right_gravity = false
|
||||
end_right_gravity = true
|
||||
elseif target == 'lock' then
|
||||
right_gravity = false
|
||||
end_right_gravity = false
|
||||
end
|
||||
|
||||
local range = self:get_range()
|
||||
self.extmark_id = vim.api.nvim_buf_set_extmark(self.bufnr, snippet_ns, range[1], range[2], {
|
||||
right_gravity = right_gravity,
|
||||
end_right_gravity = not right_gravity,
|
||||
end_right_gravity = end_right_gravity,
|
||||
end_line = range[3],
|
||||
end_col = range[4],
|
||||
hl_group = hl_group,
|
||||
@@ -181,6 +202,7 @@ end
|
||||
--- @field bufnr integer
|
||||
--- @field extmark_id integer
|
||||
--- @field tabstops table<integer, vim.snippet.Tabstop[]>
|
||||
--- @field tabstop_placements integer[]
|
||||
--- @field current_tabstop vim.snippet.Tabstop
|
||||
--- @field tab_keymaps { i: table<string, any>?, s: table<string, any>? }
|
||||
--- @field shift_tab_keymaps { i: table<string, any>?, s: table<string, any>? }
|
||||
@@ -191,14 +213,15 @@ local Session = {}
|
||||
--- @package
|
||||
--- @param bufnr integer
|
||||
--- @param snippet_extmark integer
|
||||
--- @param tabstop_data table<integer, { range: Range4, choices?: string[] }[]>
|
||||
--- @param tabstop_data table<integer, { placement: integer, range: Range4, choices?: string[] }[]>
|
||||
--- @return vim.snippet.Session
|
||||
function Session.new(bufnr, snippet_extmark, tabstop_data)
|
||||
local self = setmetatable({
|
||||
bufnr = bufnr,
|
||||
extmark_id = snippet_extmark,
|
||||
tabstops = {},
|
||||
current_tabstop = Tabstop.new(0, bufnr, { 0, 0, 0, 0 }),
|
||||
tabstop_placements = {},
|
||||
current_tabstop = Tabstop.new(0, bufnr, 0, { 0, 0, 0, 0 }),
|
||||
tab_keymaps = { i = nil, s = nil },
|
||||
shift_tab_keymaps = { i = nil, s = nil },
|
||||
}, { __index = Session })
|
||||
@@ -207,7 +230,11 @@ function Session.new(bufnr, snippet_extmark, tabstop_data)
|
||||
for index, ranges in pairs(tabstop_data) do
|
||||
for _, data in ipairs(ranges) do
|
||||
self.tabstops[index] = self.tabstops[index] or {}
|
||||
table.insert(self.tabstops[index], Tabstop.new(index, self.bufnr, data.range, data.choices))
|
||||
table.insert(
|
||||
self.tabstops[index],
|
||||
Tabstop.new(index, self.bufnr, data.placement, data.range, data.choices)
|
||||
)
|
||||
table.insert(self.tabstop_placements, data.placement)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -238,14 +265,38 @@ function Session:get_dest_index(direction)
|
||||
end
|
||||
end
|
||||
|
||||
--- Sets the right gravity of the tabstop group with the given index.
|
||||
--- Sets the right gravity for all the tabstops.
|
||||
---
|
||||
--- @package
|
||||
--- @param index integer
|
||||
--- @param right_gravity boolean
|
||||
function Session:set_group_gravity(index, right_gravity)
|
||||
function Session:set_gravity()
|
||||
local index = self.current_tabstop.index
|
||||
local all_tabstop_placements = self.tabstop_placements
|
||||
local dest_tabstop_placements = {}
|
||||
|
||||
for _, tabstop in ipairs(self.tabstops[index]) do
|
||||
tabstop:set_right_gravity(right_gravity)
|
||||
tabstop:set_gravity('expand')
|
||||
table.insert(dest_tabstop_placements, tabstop.placement)
|
||||
end
|
||||
|
||||
for i, tabstops in pairs(self.tabstops) do
|
||||
if i ~= index then
|
||||
for _, tabstop in ipairs(tabstops) do
|
||||
local placement = tabstop.placement + 1
|
||||
-- Check if there other tabstops directly adjacent
|
||||
while
|
||||
vim.list_contains(all_tabstop_placements, placement)
|
||||
and not vim.list_contains(dest_tabstop_placements, placement)
|
||||
do
|
||||
placement = placement + 1
|
||||
end
|
||||
|
||||
if vim.list_contains(dest_tabstop_placements, placement) then
|
||||
tabstop:set_gravity('lock')
|
||||
else
|
||||
tabstop:set_gravity('shift')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -443,16 +494,17 @@ function M.expand(input)
|
||||
end
|
||||
|
||||
-- Keep track of tabstop nodes during expansion.
|
||||
--- @type table<integer, { range: Range4, choices?: string[] }[]>
|
||||
--- @type table<integer, { placement: integer, range: Range4, choices?: string[] }[]>
|
||||
local tabstop_data = {}
|
||||
|
||||
--- @param placement integer
|
||||
--- @param index integer
|
||||
--- @param placeholder? string
|
||||
--- @param choices? string[]
|
||||
local function add_tabstop(index, placeholder, choices)
|
||||
local function add_tabstop(placement, index, placeholder, choices)
|
||||
tabstop_data[index] = tabstop_data[index] or {}
|
||||
local range = compute_tabstop_range(snippet_text, placeholder)
|
||||
table.insert(tabstop_data[index], { range = range, choices = choices })
|
||||
table.insert(tabstop_data[index], { placement = placement, range = range, choices = choices })
|
||||
end
|
||||
|
||||
--- Appends the given text to the snippet, taking care of indentation.
|
||||
@@ -479,23 +531,23 @@ function M.expand(input)
|
||||
table.insert(snippet_text, table.concat(lines, '\n'))
|
||||
end
|
||||
|
||||
for _, child in ipairs(snippet.data.children) do
|
||||
for index, child in ipairs(snippet.data.children) do
|
||||
local type, data = child.type, child.data
|
||||
if type == G.NodeType.Tabstop then
|
||||
--- @cast data vim.snippet.TabstopData
|
||||
local placeholder = placeholders[data.tabstop]
|
||||
add_tabstop(data.tabstop, placeholder)
|
||||
add_tabstop(index, data.tabstop, placeholder)
|
||||
if placeholder then
|
||||
append_to_snippet(placeholder)
|
||||
end
|
||||
elseif type == G.NodeType.Placeholder then
|
||||
--- @cast data vim.snippet.PlaceholderData
|
||||
local value = placeholders[data.tabstop]
|
||||
add_tabstop(data.tabstop, value)
|
||||
add_tabstop(index, data.tabstop, value)
|
||||
append_to_snippet(value)
|
||||
elseif type == G.NodeType.Choice then
|
||||
--- @cast data vim.snippet.ChoiceData
|
||||
add_tabstop(data.tabstop, nil, data.values)
|
||||
add_tabstop(index, data.tabstop, nil, data.values)
|
||||
elseif type == G.NodeType.Variable then
|
||||
--- @cast data vim.snippet.VariableData
|
||||
-- Try to get the variable's value.
|
||||
@@ -504,8 +556,9 @@ function M.expand(input)
|
||||
-- Unknown variable, make this a tabstop and use the variable name as a placeholder.
|
||||
value = data.name
|
||||
local tabstop_indexes = vim.tbl_keys(tabstop_data)
|
||||
local index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes)) + 1
|
||||
add_tabstop(index, value)
|
||||
local tabstop_index = math.max(unpack((#tabstop_indexes == 0 and { 0 }) or tabstop_indexes))
|
||||
+ 1
|
||||
add_tabstop(index, tabstop_index, value)
|
||||
end
|
||||
append_to_snippet(value)
|
||||
elseif type == G.NodeType.Text then
|
||||
@@ -519,7 +572,7 @@ function M.expand(input)
|
||||
if vim.tbl_contains(vim.tbl_keys(tabstop_data), 0) then
|
||||
assert(#tabstop_data[0] == 1, 'Snippet has multiple $0 tabstops')
|
||||
else
|
||||
add_tabstop(0)
|
||||
add_tabstop(#snippet.data.children + 1, 0)
|
||||
end
|
||||
|
||||
snippet_text = text_to_lines(snippet_text)
|
||||
@@ -579,10 +632,8 @@ function M.jump(direction)
|
||||
-- Clear the autocommands so that we can move the cursor freely while selecting the tabstop.
|
||||
vim.api.nvim_clear_autocmds({ group = snippet_group, buffer = M._session.bufnr })
|
||||
|
||||
-- Deactivate expansion of the current tabstop.
|
||||
M._session:set_group_gravity(M._session.current_tabstop.index, true)
|
||||
|
||||
M._session.current_tabstop = dest
|
||||
M._session:set_gravity()
|
||||
select_tabstop(dest)
|
||||
|
||||
-- The cursor is not on a tabstop so exit the session.
|
||||
@@ -591,9 +642,6 @@ function M.jump(direction)
|
||||
return
|
||||
end
|
||||
|
||||
-- Activate expansion of the destination tabstop.
|
||||
M._session:set_group_gravity(dest.index, false)
|
||||
|
||||
-- Restore the autocommands.
|
||||
setup_autocmds(M._session.bufnr)
|
||||
end
|
||||
|
@@ -163,6 +163,48 @@ describe('vim.snippet', function()
|
||||
eq({ 'class Foo() {', ' // Inside', '}' }, buf_lines(0))
|
||||
end)
|
||||
|
||||
it('handles directly adjacent tabstops (ascending order)', function()
|
||||
test_expand_success({ '${1:one}${2:-two}${3:-three}' }, { 'one-two-three' })
|
||||
feed('1')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('2')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('3')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
eq({ '123' }, buf_lines(0))
|
||||
end)
|
||||
|
||||
it('handles directly adjacent tabstops (descending order)', function()
|
||||
test_expand_success({ '${3:three}${2:-two}${1:-one}' }, { 'three-two-one' })
|
||||
feed('1')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('2')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('3')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
eq({ '321' }, buf_lines(0))
|
||||
end)
|
||||
|
||||
it('handles directly adjacent tabstops (mixed order)', function()
|
||||
test_expand_success({ '${3:three}${1:-one}${2:-two}' }, { 'three-one-two' })
|
||||
feed('1')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('2')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
feed('3')
|
||||
feed('<Tab>')
|
||||
poke_eventloop()
|
||||
eq({ '312' }, buf_lines(0))
|
||||
end)
|
||||
|
||||
it('handles multiline placeholders', function()
|
||||
test_expand_success(
|
||||
{ 'public void foo() {', ' ${0:// TODO Auto-generated', ' throw;}', '}' },
|
||||
|
Reference in New Issue
Block a user