mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	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:
		| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tomasz N
					Tomasz N