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 value string
---@param prefix string ---@param prefix string
---@return boolean ---@return boolean
---@return integer?
local function match_item_by_value(value, prefix) local function match_item_by_value(value, prefix)
if prefix == '' then if prefix == '' then
return true return true, nil
end end
if vim.o.completeopt:find('fuzzy') ~= nil then 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 end
if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then 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 end
return vim.startswith(value, prefix) return vim.startswith(value, prefix), nil
end end
--- Turns the result of a `textDocument/completion` request into vim-compatible --- 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_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp') local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp')
for _, item in ipairs(items) do 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 word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = '' local hl_group = ''
if if
@@ -342,6 +345,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
}, },
}, },
}, },
_fuzzy_score = score,
} }
if user_convert then if user_convert then
completion_item = vim.tbl_extend('keep', user_convert(item), completion_item) 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) table.insert(candidates, completion_item)
end end
end end
if not user_cmp then if not user_cmp then
---@diagnostic disable-next-line: no-unknown local compare_by_sortText_and_label = function(a, b)
table.sort(candidates, function(a, b)
---@type lsp.CompletionItem ---@type lsp.CompletionItem
local itema = a.user_data.nvim.lsp.completion_item local itema = a.user_data.nvim.lsp.completion_item
---@type lsp.CompletionItem ---@type lsp.CompletionItem
local itemb = b.user_data.nvim.lsp.completion_item local itemb = b.user_data.nvim.lsp.completion_item
return (itema.sortText or itema.label) < (itemb.sortText or itemb.label) 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 end
return candidates return candidates
end end

View File

@@ -1394,6 +1394,64 @@ describe('vim.lsp.completion: integration', function()
end) end)
) )
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) end)
describe("vim.lsp.completion: omnifunc + 'autocomplete'", function() describe("vim.lsp.completion: omnifunc + 'autocomplete'", function()