mirror of
https://github.com/neovim/neovim.git
synced 2026-08-25 08:31:51 +00:00
fix(lsp): completion overwrites text before the word boundary #41463
Problem: One item with an edit range before the word boundary changes the start column for all items. For tsserver `str.`, completing `str.char` can become `strcharAt` and break filtering. Solution: Prepend the missing text and filter on it.
This commit is contained in:
@@ -453,6 +453,20 @@ local function commit_chars_str(chars)
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
--- @param item lsp.CompletionItem
|
||||
--- @param lnum integer
|
||||
--- @return integer?
|
||||
local function edit_start_char(item, lnum)
|
||||
local edit = item.textEdit
|
||||
if not edit then
|
||||
return nil
|
||||
elseif edit.range and edit.range.start.line == lnum then
|
||||
return edit.range.start.character
|
||||
elseif edit.insert and edit.insert.start.line == lnum then
|
||||
return edit.insert.start.character
|
||||
end
|
||||
end
|
||||
|
||||
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
||||
--- |complete-items|.
|
||||
---
|
||||
@@ -463,6 +477,7 @@ end
|
||||
--- @param line string? current line content
|
||||
--- @param lnum integer? 0-indexed line number
|
||||
--- @param encoding string? encoding
|
||||
--- @param default_start_byte integer? 0-indexed start byte for items without an edit range
|
||||
--- @return table[]
|
||||
--- @see complete-items
|
||||
function M._lsp_to_complete_items(
|
||||
@@ -472,25 +487,27 @@ function M._lsp_to_complete_items(
|
||||
server_start_boundary,
|
||||
line,
|
||||
lnum,
|
||||
encoding
|
||||
encoding,
|
||||
default_start_byte
|
||||
)
|
||||
local items = get_items(result)
|
||||
if vim.tbl_isempty(items) then
|
||||
return {}
|
||||
end
|
||||
|
||||
---@type fun(item: lsp.CompletionItem):boolean
|
||||
---@type fun(item: lsp.CompletionItem, item_prefix: string):boolean
|
||||
local matches
|
||||
if not prefix:find('%w') then
|
||||
matches = function(_)
|
||||
matches = function(_, _)
|
||||
return true
|
||||
end
|
||||
else
|
||||
---@param item lsp.CompletionItem
|
||||
matches = function(item)
|
||||
---@param item_prefix string text the item replaces, up to the cursor
|
||||
matches = function(item, item_prefix)
|
||||
local filter_text = nonempty(item.filterText)
|
||||
if filter_text then
|
||||
return match_item_by_value(filter_text, prefix)
|
||||
return match_item_by_value(filter_text, item_prefix)
|
||||
end
|
||||
|
||||
if item.textEdit and not item.textEdit.newText then
|
||||
@@ -498,7 +515,7 @@ function M._lsp_to_complete_items(
|
||||
return true
|
||||
end
|
||||
|
||||
return match_item_by_value(item.label, prefix)
|
||||
return match_item_by_value(item.label, item_prefix)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -523,27 +540,20 @@ function M._lsp_to_complete_items(
|
||||
local all_commit_str = all_commit_chars and commit_chars_str(all_commit_chars) or nil
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
local match, score = matches(item)
|
||||
if match then
|
||||
local word = get_completion_word(item, prefix, match_item_by_value)
|
||||
|
||||
if server_start_boundary and line and lnum and encoding and item.textEdit then
|
||||
--- @type integer?
|
||||
local item_start_char
|
||||
if item.textEdit.range and item.textEdit.range.start.line == lnum then
|
||||
item_start_char = item.textEdit.range.start.character
|
||||
elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
|
||||
item_start_char = item.textEdit.insert.start.character
|
||||
end
|
||||
|
||||
if item_start_char then
|
||||
local item_start_byte = vim.str_byteindex(line, encoding, item_start_char, false)
|
||||
if item_start_byte > server_start_boundary then
|
||||
local missing_prefix = line:sub(server_start_boundary + 1, item_start_byte)
|
||||
word = missing_prefix .. word
|
||||
end
|
||||
end
|
||||
local pad, item_prefix = '', prefix
|
||||
if server_start_boundary and line and lnum and encoding then
|
||||
local start_char = edit_start_char(item, lnum)
|
||||
local start_byte = start_char and vim.str_byteindex(line, encoding, start_char, false)
|
||||
or default_start_byte
|
||||
if start_byte and start_byte > server_start_boundary then
|
||||
pad = line:sub(server_start_boundary + 1, start_byte)
|
||||
item_prefix = prefix:sub(#pad + 1)
|
||||
end
|
||||
end
|
||||
|
||||
local match, score = matches(item, item_prefix)
|
||||
if match then
|
||||
local word = pad .. get_completion_word(item, item_prefix, match_item_by_value)
|
||||
|
||||
local hl_group = ''
|
||||
if
|
||||
@@ -633,18 +643,9 @@ end
|
||||
local function adjust_start_col(lnum, line, items, encoding)
|
||||
local min_start_char = nil
|
||||
for _, item in pairs(items) do
|
||||
if item.textEdit then
|
||||
local start_char = nil
|
||||
if item.textEdit.range and item.textEdit.range.start.line == lnum then
|
||||
start_char = item.textEdit.range.start.character
|
||||
elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then
|
||||
start_char = item.textEdit.insert.start.character
|
||||
end
|
||||
if start_char then
|
||||
if not min_start_char or start_char < min_start_char then
|
||||
min_start_char = start_char
|
||||
end
|
||||
end
|
||||
local start_char = edit_start_char(item, lnum)
|
||||
if start_char and (not min_start_char or start_char < min_start_char) then
|
||||
min_start_char = start_char
|
||||
end
|
||||
end
|
||||
if min_start_char then
|
||||
@@ -697,8 +698,17 @@ function M._convert_results(
|
||||
server_start_boundary = client_start_boundary
|
||||
end
|
||||
local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
|
||||
local matches =
|
||||
M._lsp_to_complete_items(result, prefix, client_id, server_start_boundary, line, lnum, encoding)
|
||||
local default_start_byte = curstartbyte and client_start_boundary or nil
|
||||
local matches = M._lsp_to_complete_items(
|
||||
result,
|
||||
prefix,
|
||||
client_id,
|
||||
server_start_boundary,
|
||||
line,
|
||||
lnum,
|
||||
encoding,
|
||||
default_start_byte
|
||||
)
|
||||
|
||||
return matches, server_start_boundary
|
||||
end
|
||||
|
||||
@@ -1430,6 +1430,10 @@ describe('vim.lsp.completion: integration', function()
|
||||
range = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 7 } },
|
||||
},
|
||||
},
|
||||
{
|
||||
label = 'foobar',
|
||||
insertTextFormat = 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
exec_lua(function()
|
||||
@@ -1441,6 +1445,12 @@ describe('vim.lsp.completion: integration', function()
|
||||
feed('<C-Y>')
|
||||
eq('<div class="foo"></div>', n.api.nvim_get_current_line())
|
||||
eq({ 1, 17 }, n.api.nvim_win_get_cursor(0))
|
||||
|
||||
feed('<Esc>ccdiv.foo<C-x><C-O>')
|
||||
wait_for_pum()
|
||||
feed('<C-N><C-Y>')
|
||||
eq('div.foobar', n.api.nvim_get_current_line())
|
||||
|
||||
assert_cleanup_after_detach(client_id)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user