From 0197f13ed4fa71700fb4b5577a1375e4f34e2ad6 Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 17 Dec 2025 11:39:47 +0800 Subject: [PATCH] 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. --- runtime/lua/vim/lsp/completion.lua | 38 +++++++++--- .../functional/plugin/lsp/completion_spec.lua | 58 +++++++++++++++++++ 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 959422eecb..5b4ff79a53 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -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 diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index b4decc90bc..1c4ff5fd04 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -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('') + eq('w-1/2', n.api.nvim_get_current_line()) + end) end) describe("vim.lsp.completion: omnifunc + 'autocomplete'", function()