fix(lsp): use filterText as word if textEdit/label doesn't match

Problem:

With language servers like lemminx, completing xml tags like `<mo` first
shows the right candidates (`modules`) but after typing `d` the
candidates disappear.

This is because the server returns:

    [...]
    filterText = "<module",
    label = "module",
    textEdit = {
      newText = "<module>$1</module>$0",

Which resulted in `module` being used as `word`, and `module` doesn't
match the prefix `<mo`. Typing `d` causes the `complete()` filtering
mechanism to kick in and remove the entry.

Solution:

Use `<module` from the `filterText` as `word` if the textEdit/label
heuristic doesn't match.
This commit is contained in:
Mathias Fussenegger 2025-01-17 15:27:50 +01:00 committed by Mathias Fußenegger
parent 3530182ba4
commit b9e6fa7ec8
2 changed files with 42 additions and 3 deletions

View File

@ -127,8 +127,10 @@ end
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
---
--- @param item lsp.CompletionItem
--- @param prefix string
--- @param match fun(text: string, prefix: string):boolean
--- @return string
local function get_completion_word(item)
local function get_completion_word(item, prefix, match)
if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
if item.textEdit then
-- Use label instead of text if text has different starting characters.
@ -146,7 +148,12 @@ local function get_completion_word(item)
--
-- Typing `i` would remove the candidate because newText starts with `t`.
local text = parse_snippet(item.insertText or item.textEdit.newText)
return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
if item.filterText and not match(word, prefix) then
return item.filterText
else
return word
end
elseif item.insertText and item.insertText ~= '' then
return parse_snippet(item.insertText)
else
@ -276,7 +283,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
for _, item in ipairs(items) do
if matches(item) then
local word = get_completion_word(item)
local word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = ''
if
item.deprecated

View File

@ -216,6 +216,38 @@ describe('vim.lsp.completion: item conversion', function()
})
end)
it('uses filterText as word if label/newText would not match', function()
local items = {
{
filterText = '<module',
insertTextFormat = 2,
kind = 10,
label = 'module',
sortText = 'module',
textEdit = {
newText = '<module>$1</module>$0',
range = {
start = {
character = 0,
line = 0,
},
['end'] = {
character = 0,
line = 0,
},
},
},
},
}
local expected = {
{
abbr = 'module',
word = '<module',
},
}
assert_completion_matches('<mo', items, expected)
end)
it('fuzzy matches on label when filterText is missing', function()
assert_completion_matches('fo', {
{ label = 'foo' },