mirror of
https://github.com/neovim/neovim
synced 2025-07-16 17:21: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 ''
|
return ''
|
||||||
end
|
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
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
||||||
--- |complete-items|.
|
--- |complete-items|.
|
||||||
---
|
---
|
||||||
@ -244,8 +258,16 @@ function M._lsp_to_complete_items(result, prefix, client_id)
|
|||||||
else
|
else
|
||||||
---@param item lsp.CompletionItem
|
---@param item lsp.CompletionItem
|
||||||
matches = function(item)
|
matches = function(item)
|
||||||
local text = item.filterText or item.label
|
if item.filterText then
|
||||||
return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -134,10 +134,14 @@ describe('vim.lsp.completion: item conversion', function()
|
|||||||
eq(expected, result)
|
eq(expected, result)
|
||||||
end)
|
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 = {
|
local completion_list = {
|
||||||
{ label = 'foo' },
|
{ label = 'foo', textEdit = { newText = 'foo', range = range0 } },
|
||||||
{ label = 'bar' },
|
{ label = 'bar', textEdit = { newText = 'bar', range = range0 } },
|
||||||
}
|
}
|
||||||
local result = complete('fo|', completion_list)
|
local result = complete('fo|', completion_list)
|
||||||
local expected = {
|
local expected = {
|
||||||
@ -145,6 +149,10 @@ describe('vim.lsp.completion: item conversion', function()
|
|||||||
abbr = 'foo',
|
abbr = 'foo',
|
||||||
word = 'foo',
|
word = 'foo',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
abbr = 'bar',
|
||||||
|
word = 'bar',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
result = vim.tbl_map(function(x)
|
result = vim.tbl_map(function(x)
|
||||||
return {
|
return {
|
||||||
@ -152,9 +160,261 @@ describe('vim.lsp.completion: item conversion', function()
|
|||||||
word = x.word,
|
word = x.word,
|
||||||
}
|
}
|
||||||
end, result.items)
|
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)
|
eq(expected, result)
|
||||||
end)
|
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()
|
it('works on non word prefix', function()
|
||||||
local completion_list = {
|
local completion_list = {
|
||||||
{ label = ' foo', insertText = '->foo' },
|
{ label = ' foo', insertText = '->foo' },
|
||||||
|
Reference in New Issue
Block a user