Skip to content

Commit

Permalink
feat: implement hybrid sort
Browse files Browse the repository at this point in the history
  • Loading branch information
Saghen committed Dec 16, 2024
1 parent 76230d5 commit bfce8cf
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 21 deletions.
53 changes: 36 additions & 17 deletions lua/blink/cmp/config/fuzzy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
--- @field use_typo_resistance boolean When enabled, allows for a number of typos relative to the length of the query. Disabling this matches the behavior of fzf
--- @field use_frecency boolean Tracks the most recently/frequently used items and boosts the score of the item
--- @field use_proximity boolean Boosts the score of items matching nearby words
--- @field sorts ("label" | "sort_text" | "kind" | "score" | blink.cmp.SortFunction)[] Controls which sorts to use and in which order, these three are currently the only allowed options
--- @field sort blink.cmp.FuzzySortConfig
--- @field prebuilt_binaries blink.cmp.PrebuiltBinariesConfig

--- @class (exact) blink.cmp.PrebuiltBinariesConfig
Expand All @@ -11,16 +11,24 @@
--- @field force_system_triple? string When downloading a prebuilt binary, force the downloader to use this system triple. If this is unset then the downloader will attempt to infer the system triple from `jit.os` and `jit.arch`. Check the latest release for all available system triples. WARN: Beware that `main` may be incompatible with the version you select
--- @field extra_curl_args? string[] Extra arguments that will be passed to curl like { 'curl', ..extra_curl_args, ..built_in_args }

--- @alias blink.cmp.SortFunction fun(a: blink.cmp.CompletionItem, b: blink.cmp.CompletionItem): boolean | nil
--- @alias blink.cmp.SortFunction fun(a: blink.cmp.CompletionItem, b: blink.cmp.CompletionItem): boolean | nil Fallbacks to the next sort function when returning nil
--- @alias blink.cmp.SortFunctions ("label" | "sort_text" | "kind" | "score" | blink.cmp.SortFunction)[] Controls which sorts to use and in which order, falling back when the sort function returns nil

--- @class blink.cmp.FuzzySortConfig
--- @field strong_match blink.cmp.SortFunctions Controls which sorts to use and in which order, for strong matches (based on fuzzy match score)
--- @field weak_match blink.cmp.SortFunctions Controls which sorts to use and in which order, for weak matches (based on fuzzy match score)

local validate = require('blink.cmp.config.utils').validate
local fuzzy = {
--- @type blink.cmp.FuzzyConfig
default = {
use_typo_resistance = true,
use_frecency = true,
use_proximity = true,
sorts = { 'score', 'sort_text' },
use_frecency = false,
use_proximity = false,
sort = {
strong_match = { 'score', 'sort_text' },
weak_match = { 'sort_text', 'score' },
},
prebuilt_binaries = {
download = true,
force_version = nil,
Expand All @@ -35,18 +43,7 @@ function fuzzy.validate(config)
use_typo_resistance = { config.use_typo_resistance, 'boolean' },
use_frecency = { config.use_frecency, 'boolean' },
use_proximity = { config.use_proximity, 'boolean' },
sorts = {
config.sorts,
function(sorts)
for _, sort in ipairs(sorts) do
if not vim.tbl_contains({ 'label', 'sort_text', 'kind', 'score' }, sort) and type(sort) ~= 'function' then
return false
end
end
return true
end,
'one of: "label", "sort_text", "kind", "score" or a function',
},
sort = { config.sort, 'table' },
prebuilt_binaries = { config.prebuilt_binaries, 'table' },
}, config)
validate('fuzzy.prebuilt_binaries', {
Expand All @@ -55,6 +52,28 @@ function fuzzy.validate(config)
force_system_triple = { config.prebuilt_binaries.force_system_triple, { 'string', 'nil' } },
extra_curl_args = { config.prebuilt_binaries.extra_curl_args, { 'table' } },
}, config.prebuilt_binaries)

--- @param sorts blink.cmp.SortFunctions
local function validate_sort(sorts)
for _, sort in ipairs(sorts) do
if not vim.tbl_contains({ 'label', 'sort_text', 'kind', 'score' }, sort) and type(sort) ~= 'function' then
return false
end
end
return true
end
validate('fuzzy.sort', {
strong_match = {
config.sort.strong_match,
validate_sort,
'one of: "label", "sort_text", "kind", "score" or a function',
},
weak_match = {
config.sort.weak_match,
validate_sort,
'one of: "label", "sort_text", "kind", "score" or a function',
},
}, config.sort)
end

return fuzzy
8 changes: 6 additions & 2 deletions lua/blink/cmp/fuzzy/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ function fuzzy.fuzzy(needle, haystacks_by_provider)
use_typo_resistance = config.fuzzy.use_typo_resistance,
use_frecency = config.fuzzy.use_frecency and #needle > 0,
use_proximity = config.fuzzy.use_proximity and #needle > 0,
sorts = config.fuzzy.sorts,
nearby_words = nearby_words,
})

Expand All @@ -76,7 +75,12 @@ function fuzzy.fuzzy(needle, haystacks_by_provider)
end
end

return require('blink.cmp.fuzzy.sort').sort(filtered_items, config.fuzzy.sorts)
return require('blink.cmp.fuzzy.sort').sort(
filtered_items,
6 * needle:len(),
config.fuzzy.sort.strong_match,
config.fuzzy.sort.weak_match
)
end

return fuzzy
35 changes: 33 additions & 2 deletions lua/blink/cmp/fuzzy/sort.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
local sort = {}

--- Similar to Zed, we split the list into two buckets, sort them separately and combine.
--- By default, the strong matches will be sorted by score and then sort_text, while the weak
--- matches will be sorted by sort_text and then score.
--- https://github.com/zed-industries/zed/blob/f64fcedab/crates/editor/src/code_context_menus.rs#L553-L566
--- @param list blink.cmp.CompletionItem[]
--- @param funcs ("label" | "sort_text" | "kind" | "score" | blink.cmp.SortFunction)[]
--- @param score_threshold number
--- @param strong_match_funcs blink.cmp.SortFunctions
--- @param weak_match_funcs blink.cmp.SortFunctions
--- @return blink.cmp.CompletionItem[]
function sort.sort(list, funcs)
function sort.sort(list, score_threshold, strong_match_funcs, weak_match_funcs)
local strong_matches, weak_matches = sort.partition_by_score(list, score_threshold)

sort.list(strong_matches, strong_match_funcs)
sort.list(weak_matches, weak_match_funcs)

return vim.list_extend(strong_matches, weak_matches)
end

function sort.partition_by_score(list, score_threshold)
local above = {}
local below = {}
for _, item in ipairs(list) do
if item.score >= score_threshold then
table.insert(above, item)
else
table.insert(below, item)
end
end
return above, below
end

--- @param list blink.cmp.CompletionItem[]
--- @param funcs blink.cmp.SortFunctions
--- @return blink.cmp.CompletionItem[]
function sort.list(list, funcs)
local sorting_funcs = vim.tbl_map(
function(name_or_func) return type(name_or_func) == 'string' and sort[name_or_func] or name_or_func end,
funcs
Expand Down

0 comments on commit bfce8cf

Please sign in to comment.