fix(lsp): filter completion candidates based on completeopt (#30945)

This commit is contained in:
Kristijan Husak
2024-11-13 16:18:29 +01:00
committed by GitHub
parent 36990f324d
commit 33d10db5b7
2 changed files with 287 additions and 5 deletions

View File

@ -220,6 +220,20 @@ local function get_doc(item)
return ''
end
---@param value string
---@param prefix string
---@return boolean
local function match_item_by_value(value, prefix)
if vim.o.completeopt:find('fuzzy') ~= nil then
return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
end
if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
return vim.startswith(value:lower(), prefix:lower())
end
return vim.startswith(value, prefix)
end
--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
@ -244,8 +258,16 @@ function M._lsp_to_complete_items(result, prefix, client_id)
else
---@param item lsp.CompletionItem
matches = function(item)
local text = item.filterText or item.label
return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil
if item.filterText then
return match_item_by_value(item.filterText, prefix)
end
if item.textEdit then
-- server took care of filtering
return true
end
return match_item_by_value(item.label, prefix)
end
end

View File

@ -134,10 +134,14 @@ describe('vim.lsp.completion: item conversion', function()
eq(expected, result)
end)
it('filters on label if filterText is missing', function()
it('does not filter if there is a textEdit', function()
local range0 = {
start = { line = 0, character = 0 },
['end'] = { line = 0, character = 0 },
}
local completion_list = {
{ label = 'foo' },
{ label = 'bar' },
{ label = 'foo', textEdit = { newText = 'foo', range = range0 } },
{ label = 'bar', textEdit = { newText = 'bar', range = range0 } },
}
local result = complete('fo|', completion_list)
local expected = {
@ -145,6 +149,10 @@ describe('vim.lsp.completion: item conversion', function()
abbr = 'foo',
word = 'foo',
},
{
abbr = 'bar',
word = 'bar',
},
}
result = vim.tbl_map(function(x)
return {
@ -152,9 +160,261 @@ describe('vim.lsp.completion: item conversion', function()
word = x.word,
}
end, result.items)
local sorter = function(a, b)
return a.word > b.word
end
table.sort(expected, sorter)
table.sort(result, sorter)
eq(expected, result)
end)
---@param prefix string
---@param items lsp.CompletionItem[]
---@param expected table[]
local assert_completion_matches = function(prefix, items, expected)
local result = complete(prefix .. '|', items)
result = vim.tbl_map(function(x)
return {
abbr = x.abbr,
word = x.word,
}
end, result.items)
local sorter = function(a, b)
return a.word > b.word
end
table.sort(expected, sorter)
table.sort(result, sorter)
eq(expected, result)
end
describe('when completeopt has fuzzy matching enabled', function()
before_each(function()
exec_lua(function()
vim.opt.completeopt:append('fuzzy')
end)
end)
after_each(function()
exec_lua(function()
vim.opt.completeopt:remove('fuzzy')
end)
end)
it('fuzzy matches on filterText', function()
assert_completion_matches('fo', {
{ label = '?.foo', filterText = 'foo' },
{ label = 'faz other', filterText = 'faz other' },
{ label = 'bar', filterText = 'bar' },
}, {
{
abbr = 'faz other',
word = 'faz other',
},
{
abbr = '?.foo',
word = '?.foo',
},
})
end)
it('fuzzy matches on label when filterText is missing', function()
assert_completion_matches('fo', {
{ label = 'foo' },
{ label = 'faz other' },
{ label = 'bar' },
}, {
{
abbr = 'faz other',
word = 'faz other',
},
{
abbr = 'foo',
word = 'foo',
},
})
end)
end)
describe('when smartcase is enabled', function()
before_each(function()
exec_lua(function()
vim.opt.smartcase = true
end)
end)
after_each(function()
exec_lua(function()
vim.opt.smartcase = false
end)
end)
it('matches filterText case sensitively', function()
assert_completion_matches('Fo', {
{ label = 'foo', filterText = 'foo' },
{ label = '?.Foo', filterText = 'Foo' },
{ label = 'Faz other', filterText = 'Faz other' },
{ label = 'faz other', filterText = 'faz other' },
{ label = 'bar', filterText = 'bar' },
}, {
{
abbr = '?.Foo',
word = '?.Foo',
},
})
end)
it('matches label case sensitively when filterText is missing', function()
assert_completion_matches('Fo', {
{ label = 'foo' },
{ label = 'Foo' },
{ label = 'Faz other' },
{ label = 'faz other' },
{ label = 'bar' },
}, {
{
abbr = 'Foo',
word = 'Foo',
},
})
end)
describe('when ignorecase is enabled', function()
before_each(function()
exec_lua(function()
vim.opt.ignorecase = true
end)
end)
after_each(function()
exec_lua(function()
vim.opt.ignorecase = false
end)
end)
it('matches filterText case insensitively if prefix is lowercase', function()
assert_completion_matches('fo', {
{ label = '?.foo', filterText = 'foo' },
{ label = '?.Foo', filterText = 'Foo' },
{ label = 'Faz other', filterText = 'Faz other' },
{ label = 'faz other', filterText = 'faz other' },
{ label = 'bar', filterText = 'bar' },
}, {
{
abbr = '?.Foo',
word = '?.Foo',
},
{
abbr = '?.foo',
word = '?.foo',
},
})
end)
it(
'matches label case insensitively if prefix is lowercase and filterText is missing',
function()
assert_completion_matches('fo', {
{ label = 'foo' },
{ label = 'Foo' },
{ label = 'Faz other' },
{ label = 'faz other' },
{ label = 'bar' },
}, {
{
abbr = 'Foo',
word = 'Foo',
},
{
abbr = 'foo',
word = 'foo',
},
})
end
)
it('matches filterText case sensitively if prefix has uppercase letters', function()
assert_completion_matches('Fo', {
{ label = 'foo', filterText = 'foo' },
{ label = '?.Foo', filterText = 'Foo' },
{ label = 'Faz other', filterText = 'Faz other' },
{ label = 'faz other', filterText = 'faz other' },
{ label = 'bar', filterText = 'bar' },
}, {
{
abbr = '?.Foo',
word = '?.Foo',
},
})
end)
it(
'matches label case sensitively if prefix has uppercase letters and filterText is missing',
function()
assert_completion_matches('Fo', {
{ label = 'foo' },
{ label = 'Foo' },
{ label = 'Faz other' },
{ label = 'faz other' },
{ label = 'bar' },
}, {
{
abbr = 'Foo',
word = 'Foo',
},
})
end
)
end)
end)
describe('when ignorecase is enabled', function()
before_each(function()
exec_lua(function()
vim.opt.ignorecase = true
end)
end)
after_each(function()
exec_lua(function()
vim.opt.ignorecase = false
end)
end)
it('matches filterText case insensitively', function()
assert_completion_matches('Fo', {
{ label = '?.foo', filterText = 'foo' },
{ label = '?.Foo', filterText = 'Foo' },
{ label = 'Faz other', filterText = 'Faz other' },
{ label = 'faz other', filterText = 'faz other' },
{ label = 'bar', filterText = 'bar' },
}, {
{
abbr = '?.Foo',
word = '?.Foo',
},
{
abbr = '?.foo',
word = '?.foo',
},
})
end)
it('matches label case insensitively when filterText is missing', function()
assert_completion_matches('Fo', {
{ label = 'foo' },
{ label = 'Foo' },
{ label = 'Faz other' },
{ label = 'faz other' },
{ label = 'bar' },
}, {
{
abbr = 'Foo',
word = 'Foo',
},
{
abbr = 'foo',
word = 'foo',
},
})
end)
end)
it('works on non word prefix', function()
local completion_list = {
{ label = ' foo', insertText = '->foo' },