mirror of
https://github.com/neovim/neovim
synced 2025-07-15 16:51:49 +00:00
fix(lsp): filter completion candidates based on completeopt (#30945)
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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' },
|
||||
|
Reference in New Issue
Block a user