commit 0197f13ed4fa71700fb4b5577a1375e4f34e2ad6
parent 8a94daf80eadbd9179768fe3d7da2f06c81dc740
Author: glepnir <glephunter@gmail.com>
Date: Wed, 17 Dec 2025 11:39:47 +0800
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.
Diffstat:
2 files changed, 88 insertions(+), 8 deletions(-)
diff --git 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
@@ -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()