fix(lsp): sort items when completeopt include fuzzy #36974

Problem: When fuzzy is enabled and the prefix is not empty,
items are not sorted by fuzzy score before calling fn.complete.

Solution: Use matchfuzzypos to get the scores and sort the items
by fuzzy score before calling fn.complete.
This commit is contained in:
glepnir
2025-12-17 11:39:47 +08:00
committed by GitHub
parent 8a94daf80e
commit 0197f13ed4
2 changed files with 88 additions and 8 deletions

View File

@@ -260,18 +260,20 @@ end
---@param value string
---@param prefix string
---@return boolean
---@return integer?
local function match_item_by_value(value, prefix)
if prefix == '' then
return true
return true, nil
end
if vim.o.completeopt:find('fuzzy') ~= nil then
return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
local score = vim.fn.matchfuzzypos({ value }, prefix)[3] ---@type table
return #score > 0, score[1]
end
if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
return vim.startswith(value:lower(), prefix:lower())
return vim.startswith(value:lower(), prefix:lower()), nil
end
return vim.startswith(value, prefix)
return vim.startswith(value, prefix), nil
end
--- Turns the result of a `textDocument/completion` request into vim-compatible
@@ -315,7 +317,8 @@ function M._lsp_to_complete_items(result, prefix, client_id)
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
for _, item in ipairs(items) do
if matches(item) then
local match, score = matches(item)
if match then
local word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = ''
if
@@ -342,6 +345,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
},
},
},
_fuzzy_score = score,
}
if user_convert then
completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
@@ -349,15 +353,33 @@ function M._lsp_to_complete_items(result, prefix, client_id)
table.insert(candidates, completion_item)
end
end
if not user_cmp then
---@diagnostic disable-next-line: no-unknown
table.sort(candidates, function(a, b)
local compare_by_sortText_and_label = function(a, b)
---@type lsp.CompletionItem
local itema = a.user_data.nvim.lsp.completion_item
---@type lsp.CompletionItem
local itemb = b.user_data.nvim.lsp.completion_item
return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
end)
end
local use_fuzzy_sort = vim.o.completeopt:find('fuzzy') ~= nil
and vim.o.completeopt:find('nosort') == nil
and not result.isIncomplete
and #prefix > 0
local compare_fn = use_fuzzy_sort
and function(a, b)
local score_a = a._fuzzy_score or 0
local score_b = b._fuzzy_score or 0
if score_a ~= score_b then
return score_a > score_b
end
return compare_by_sortText_and_label(a, b)
end
or compare_by_sortText_and_label
table.sort(candidates, compare_fn)
end
return candidates
end

View File

@@ -1394,6 +1394,64 @@ describe('vim.lsp.completion: integration', function()
end)
)
end)
it('sorts items when fuzzy is enabled and prefix not empty #33610', function()
local completion_list = {
isIncomplete = false,
items = {
{
kind = 21,
label = '-row-end-1',
sortText = '0327',
textEdit = {
newText = '-row-end-1',
range = {
['end'] = {
character = 1,
line = 0,
},
start = {
character = 0,
line = 0,
},
},
},
},
{
kind = 21,
label = 'w-1/2',
sortText = '3052',
textEdit = {
newText = 'w-1/2',
range = {
['end'] = {
character = 1,
line = 0,
},
start = {
character = 0,
line = 0,
},
},
},
},
},
}
exec_lua(function()
vim.o.completeopt = 'menuone,fuzzy'
end)
create_server('dummy', completion_list, { trigger_chars = { '-' } })
feed('Sw-')
retry(nil, nil, function()
eq(
1,
exec_lua(function()
return vim.fn.pumvisible()
end)
)
end)
feed('<C-y>')
eq('w-1/2', n.api.nvim_get_current_line())
end)
end)
describe("vim.lsp.completion: omnifunc + 'autocomplete'", function()