fix(lsp): fix cursor position after snippet expansion (#30659)

Problem: on `CompleteDone` cursor can jump to the end of line instead of
the end of the completed word.

Solution: remove only inserted word for snippet expansion instead of everything
until eol.

Fixes #30656

Co-authored-by: Mathias Fussenegger <f.mathias@zignar.net>
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
Tomasz N 2024-10-10 11:40:03 +02:00 committed by GitHub
parent 641c4b1a2a
commit b3109084c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 49 deletions

View File

@ -113,12 +113,11 @@ local function parse_snippet(input)
end end
--- @param item lsp.CompletionItem --- @param item lsp.CompletionItem
--- @param suffix? string local function apply_snippet(item)
local function apply_snippet(item, suffix)
if item.textEdit then if item.textEdit then
vim.snippet.expand(item.textEdit.newText .. suffix) vim.snippet.expand(item.textEdit.newText)
elseif item.insertText then elseif item.insertText then
vim.snippet.expand(item.insertText .. suffix) vim.snippet.expand(item.insertText)
end end
end end
@ -539,15 +538,12 @@ local function on_complete_done()
-- Remove the already inserted word. -- Remove the already inserted word.
local start_char = cursor_col - #completed_item.word local start_char = cursor_col - #completed_item.word
local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1] api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, cursor_col, { '' })
api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' })
return line:sub(cursor_col + 1)
end end
--- @param suffix? string local function apply_snippet_and_command()
local function apply_snippet_and_command(suffix)
if expand_snippet then if expand_snippet then
apply_snippet(completion_item, suffix) apply_snippet(completion_item)
end end
local command = completion_item.command local command = completion_item.command
@ -565,9 +561,9 @@ local function on_complete_done()
end end
if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
local suffix = clear_word() clear_word()
lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding) lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding)
apply_snippet_and_command(suffix) apply_snippet_and_command()
elseif resolve_provider and type(completion_item) == 'table' then elseif resolve_provider and type(completion_item) == 'table' then
local changedtick = vim.b[bufnr].changedtick local changedtick = vim.b[bufnr].changedtick
@ -577,7 +573,7 @@ local function on_complete_done()
return return
end end
local suffix = clear_word() clear_word()
if err then if err then
vim.notify_once(err.message, vim.log.levels.WARN) vim.notify_once(err.message, vim.log.levels.WARN)
elseif result and result.additionalTextEdits then elseif result and result.additionalTextEdits then
@ -587,11 +583,11 @@ local function on_complete_done()
end end
end end
apply_snippet_and_command(suffix) apply_snippet_and_command()
end, bufnr) end, bufnr)
else else
local suffix = clear_word() clear_word()
apply_snippet_and_command(suffix) apply_snippet_and_command()
end end
end end

View File

@ -471,6 +471,39 @@ describe('vim.lsp.completion: item conversion', function()
) )
end) end)
--- @param completion_result lsp.CompletionList
--- @return integer
local function create_server(completion_result)
return exec_lua(function()
local server = _G._create_server({
capabilities = {
completionProvider = {
triggerCharacters = { '.' },
},
},
handlers = {
['textDocument/completion'] = function(_, _, callback)
callback(nil, completion_result)
end,
},
})
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_win_set_buf(0, bufnr)
return vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
on_attach = function(client, bufnr0)
vim.lsp.completion.enable(true, client.id, bufnr0, {
convert = function(item)
return { abbr = item.label:gsub('%b()', '') }
end,
})
end,
})
end)
end
describe('vim.lsp.completion: protocol', function() describe('vim.lsp.completion: protocol', function()
before_each(function() before_each(function()
clear() clear()
@ -487,39 +520,6 @@ describe('vim.lsp.completion: protocol', function()
after_each(clear) after_each(clear)
--- @param completion_result lsp.CompletionList
--- @return integer
local function create_server(completion_result)
return exec_lua(function()
local server = _G._create_server({
capabilities = {
completionProvider = {
triggerCharacters = { '.' },
},
},
handlers = {
['textDocument/completion'] = function(_, _, callback)
callback(nil, completion_result)
end,
},
})
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_win_set_buf(0, bufnr)
return vim.lsp.start({
name = 'dummy',
cmd = server.cmd,
on_attach = function(client, bufnr0)
vim.lsp.completion.enable(true, client.id, bufnr0, {
convert = function(item)
return { abbr = item.label:gsub('%b()', '') }
end,
})
end,
})
end)
end
local function assert_matches(fn) local function assert_matches(fn)
retry(nil, nil, function() retry(nil, nil, function()
fn(exec_lua('return _G.capture.matches')) fn(exec_lua('return _G.capture.matches'))
@ -726,3 +726,58 @@ describe('vim.lsp.completion: protocol', function()
end) end)
end) end)
end) end)
describe('vim.lsp.completion: integration', function()
before_each(function()
clear()
exec_lua(create_server_definition)
exec_lua(function()
vim.fn.complete = vim.schedule_wrap(vim.fn.complete)
end)
end)
after_each(clear)
it('puts cursor at the end of completed word', function()
local completion_list = {
isIncomplete = false,
items = {
{
label = 'hello',
insertText = '${1:hello} friends',
insertTextFormat = 2,
},
},
}
exec_lua(function()
vim.o.completeopt = 'menuone,noselect'
end)
create_server(completion_list)
feed('i world<esc>0ih<c-x><c-o>')
retry(nil, nil, function()
eq(
1,
exec_lua(function()
return vim.fn.pumvisible()
end)
)
end)
feed('<C-n><C-y>')
eq(
{ true, { 'hello friends world' } },
exec_lua(function()
return {
vim.snippet.active({ direction = 1 }),
vim.api.nvim_buf_get_lines(0, 0, -1, true),
}
end)
)
feed('<tab>')
eq(
#'hello friends',
exec_lua(function()
return vim.api.nvim_win_get_cursor(0)[2]
end)
)
end)
end)