diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index f1a17e3c4a..65bfc25446 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -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 diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 775e3e7dd8..81ee51c954 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -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('') eq('
', n.api.nvim_get_current_line()) eq({ 1, 17 }, n.api.nvim_win_get_cursor(0)) + + feed('ccdiv.foo') + wait_for_pum() + feed('') + eq('div.foobar', n.api.nvim_get_current_line()) + assert_cleanup_after_detach(client_id) end)