Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement hybrid sort #593

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading