mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +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,22 +471,6 @@ describe('vim.lsp.completion: item conversion', function()
 | 
				
			|||||||
  )
 | 
					  )
 | 
				
			||||||
end)
 | 
					end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('vim.lsp.completion: protocol', function()
 | 
					 | 
				
			||||||
  before_each(function()
 | 
					 | 
				
			||||||
    clear()
 | 
					 | 
				
			||||||
    exec_lua(create_server_definition)
 | 
					 | 
				
			||||||
    exec_lua(function()
 | 
					 | 
				
			||||||
      _G.capture = {}
 | 
					 | 
				
			||||||
      --- @diagnostic disable-next-line:duplicate-set-field
 | 
					 | 
				
			||||||
      vim.fn.complete = function(col, matches)
 | 
					 | 
				
			||||||
        _G.capture.col = col
 | 
					 | 
				
			||||||
        _G.capture.matches = matches
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end)
 | 
					 | 
				
			||||||
  end)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  after_each(clear)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
--- @param completion_result lsp.CompletionList
 | 
					--- @param completion_result lsp.CompletionList
 | 
				
			||||||
--- @return integer
 | 
					--- @return integer
 | 
				
			||||||
local function create_server(completion_result)
 | 
					local function create_server(completion_result)
 | 
				
			||||||
@@ -520,6 +504,22 @@ describe('vim.lsp.completion: protocol', function()
 | 
				
			|||||||
  end)
 | 
					  end)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('vim.lsp.completion: protocol', function()
 | 
				
			||||||
 | 
					  before_each(function()
 | 
				
			||||||
 | 
					    clear()
 | 
				
			||||||
 | 
					    exec_lua(create_server_definition)
 | 
				
			||||||
 | 
					    exec_lua(function()
 | 
				
			||||||
 | 
					      _G.capture = {}
 | 
				
			||||||
 | 
					      --- @diagnostic disable-next-line:duplicate-set-field
 | 
				
			||||||
 | 
					      vim.fn.complete = function(col, matches)
 | 
				
			||||||
 | 
					        _G.capture.col = col
 | 
				
			||||||
 | 
					        _G.capture.matches = matches
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  after_each(clear)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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