fix(lsp): fallback to filterText for non-matching PlainText items #39695

Problem:
PlainText completion items used `textEdit.newText` or `insertText` as
the completion word even when they did not match the typed prefix. This
could break popup completion behavior like 'completeopt+=longest'.

Solution:
Fall back to `filterText` when `newText` or `insertText` does not match
the typed prefix.
This commit is contained in:
glepnir
2026-05-17 23:58:49 +08:00
committed by GitHub
parent a562fb33ca
commit 767fbd88ff
2 changed files with 61 additions and 26 deletions

View File

@@ -211,8 +211,15 @@ local function get_completion_word(item, prefix, match)
elseif item.textEdit then
local word = item.textEdit.newText
word = string.gsub(word, '\r\n?', '\n')
return word:match('([^\n]*)') or word
word = word:match('([^\n]*)') or word
if item.filterText and not match(word, prefix) then
return item.filterText
end
return word
elseif item.insertText and item.insertText ~= '' then
if item.filterText and not match(item.insertText, prefix) then
return item.filterText
end
return item.insertText
end
return item.label

View File

@@ -222,6 +222,59 @@ describe('vim.lsp.completion: item conversion', function()
eq(expected, got)
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 },
},
},
},
{
filterText = 'atto',
insertTextFormat = 1,
kind = 7,
label = '•std::atto',
sortText = 'atto',
textEdit = {
newText = 'std::atto',
range = {
start = { character = 0, line = 0 },
['end'] = { character = 0, line = 0 },
},
},
},
{
filterText = 'adopt_lock_t',
insertTextFormat = 1,
kind = 7,
label = '•std::adopt_lock_t',
sortText = 'adopt_lock_t',
insertText = 'std::adopt_lock_t',
},
}
assert_completion_matches('<mo', items, {
{ abbr = 'module', word = '<module' },
})
assert_completion_matches('a', items, {
{ abbr = '•std::atto', word = 'atto' },
{ abbr = '•std::adopt_lock_t', word = 'adopt_lock_t' },
})
assert_completion_matches('', items, {
{ abbr = 'module', word = 'module' },
{ abbr = '•std::atto', word = 'std::atto' },
{ abbr = '•std::adopt_lock_t', word = 'std::adopt_lock_t' },
})
end)
describe('when completeopt has fuzzy matching enabled', function()
before_each(function()
exec_lua(function()
@@ -245,31 +298,6 @@ 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 },
},
},
},
}
assert_completion_matches('<mo', items, {
{ abbr = 'module', word = '<module' },
})
assert_completion_matches('', items, {
{ abbr = 'module', word = 'module' },
})
end)
it('fuzzy matches on label when filterText is missing', function()
assert_completion_matches('fo', {
{ label = 'foo' },