feat: add fuzzy matching sorter experimentally (#166)

* feat: add fuzzy matching sorter experimentally

* feat: add an option to select matcher logic

* feat: separate logic to match: fuzzy, fuzzy_full

* Revert "feat: separate logic to match: fuzzy, fuzzy_full"

This reverts commit 64c022904871143ab12c7d6ba29c89fbabdbe15e.

* feat: use fzy sorter and combine recency scores

* feat: enable to change logic to calculate scores

* feat: change the view for scores by config.matcher

* docs: add note in README

* docs: add note for `scoring_function`
This commit is contained in:
JINNOUCHI Yasushi 2024-04-28 17:58:45 +09:00 committed by GitHub
parent 94a532cb9c
commit 42b6421061
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 2 deletions

View File

@ -218,6 +218,22 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel
Patterns in this table control which files are indexed (and subsequently Patterns in this table control which files are indexed (and subsequently
which you'll see in the finder results). which you'll see in the finder results).
- `matcher` (default: `"default"`)
> ___CAUTION___<br>
> This option is highly experimental.
In default, it matches against candidates by the so-called “substr matcher”,
that is, you should input characters ordered properly. If you set here with
`"fuzzy"`, it uses [_fzy_ matcher][fzy] implemented in telescope itself, and
combines the result with recency scores. With this, you can select candidates
fully _fuzzily_, besides that, can select easily ones that has higher recency
scores.
See the discussion in https://github.com/nvim-telescope/telescope-frecency.nvim/issues/165.
[fzy]: https://github.com/jhawthorn/fzy
- `max_timestamps` (default: `10`) - `max_timestamps` (default: `10`)
Set the max count of timestamps DB keeps when you open files. It ignores the Set the max count of timestamps DB keeps when you open files. It ignores the
@ -245,6 +261,27 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel
} }
``` ```
- `scoring_function` (default: see below)
> ___CAUTION___<br>
> This option is highly experimental.
This will be used only when `matcher` option is `"fuzzy"`. You can customize the
logic to adjust scores between [fzy matcher][fzy] scores and recency ones.
```lua
-- the default value
---@param recency integer
---@param fzy_score number
---@return number
scoring_function = function(recency, fzy_score)
return (10 / (recency == 0 and 1 or recency)) - 1 / fzy_score
end,
```
NOTE: telescope orders candidates in the ascending order. It also accepts
negative numbers, but `-1` means the candidates should not be shown.
- `show_filter_column` (default: `true`) - `show_filter_column` (default: `true`)
Show the path of the active filter before file paths. In default, it uses the Show the path of the active filter before file paths. In default, it uses the

View File

@ -15,6 +15,8 @@ local Config = {}
---@field filter_delimiter string default: ":" ---@field filter_delimiter string default: ":"
---@field hide_current_buffer boolean default: false ---@field hide_current_buffer boolean default: false
---@field ignore_patterns string[] default: { "*.git/*", "*/tmp/*", "term://*" } ---@field ignore_patterns string[] default: { "*.git/*", "*/tmp/*", "term://*" }
---@field matcher "default"|"fuzzy" default: "default"
---@field scoring_function fun(recency: integer, fzy_score: number): number default: see lua/frecency/config.lua
---@field max_timestamps integer default: 10 ---@field max_timestamps integer default: 10
---@field show_filter_column boolean|string[] default: true ---@field show_filter_column boolean|string[] default: true
---@field show_scores boolean default: false ---@field show_scores boolean default: false
@ -35,6 +37,7 @@ Config.new = function()
hide_current_buffer = false, hide_current_buffer = false,
ignore_patterns = os_util.is_windows and { [[*.git\*]], [[*\tmp\*]], "term://*" } ignore_patterns = os_util.is_windows and { [[*.git\*]], [[*\tmp\*]], "term://*" }
or { "*.git/*", "*/tmp/*", "term://*" }, or { "*.git/*", "*/tmp/*", "term://*" },
matcher = "default",
max_timestamps = 10, max_timestamps = 10,
recency_values = { recency_values = {
{ age = 240, value = 100 }, -- past 4 hours { age = 240, value = 100 }, -- past 4 hours
@ -44,6 +47,12 @@ Config.new = function()
{ age = 43200, value = 20 }, -- past month { age = 43200, value = 20 }, -- past month
{ age = 129600, value = 10 }, -- past 90 days { age = 129600, value = 10 }, -- past 90 days
}, },
---@param recency integer
---@param fzy_score number
---@return number
scoring_function = function(recency, fzy_score)
return (10 / (recency == 0 and 1 or recency)) - 1 / fzy_score
end,
show_filter_column = true, show_filter_column = true,
show_scores = false, show_scores = false,
show_unindexed = true, show_unindexed = true,
@ -62,7 +71,9 @@ Config.new = function()
filter_delimiter = true, filter_delimiter = true,
hide_current_buffer = true, hide_current_buffer = true,
ignore_patterns = true, ignore_patterns = true,
matcher = true,
max_timestamps = true, max_timestamps = true,
scoring_function = true,
show_filter_column = true, show_filter_column = true,
show_scores = true, show_scores = true,
show_unindexed = true, show_unindexed = true,
@ -105,6 +116,13 @@ Config.setup = function(ext_config)
filter_delimiter = { opts.filter_delimiter, "s" }, filter_delimiter = { opts.filter_delimiter, "s" },
hide_current_buffer = { opts.hide_current_buffer, "b" }, hide_current_buffer = { opts.hide_current_buffer, "b" },
ignore_patterns = { opts.ignore_patterns, "t" }, ignore_patterns = { opts.ignore_patterns, "t" },
matcher = {
opts.matcher,
function(v)
return type(v) == "string" and (v == "default" or v == "fuzzy")
end,
'"default" or "fuzzy"',
},
max_timestamps = { max_timestamps = {
opts.max_timestamps, opts.max_timestamps,
function(v) function(v)

View File

@ -33,6 +33,7 @@ end
---@field ordinal string ---@field ordinal string
---@field name string ---@field name string
---@field score number ---@field score number
---@field fuzzy_score? number
---@field display fun(entry: FrecencyEntry): string, table ---@field display fun(entry: FrecencyEntry): string, table
---@class FrecencyFile ---@class FrecencyFile
@ -77,7 +78,11 @@ end
function EntryMaker:displayer_items(workspace, workspace_tag) function EntryMaker:displayer_items(workspace, workspace_tag)
local items = {} local items = {}
if config.show_scores then if config.show_scores then
table.insert(items, { width = 8 }) table.insert(items, { width = 5 }) -- recency score
if config.matcher == "fuzzy" then
table.insert(items, { width = 5 }) -- index
table.insert(items, { width = 6 }) -- fuzzy score
end
end end
if self.web_devicons.is_enabled then if self.web_devicons.is_enabled then
table.insert(items, { width = 2 }) table.insert(items, { width = 2 })
@ -99,6 +104,12 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
local items = {} local items = {}
if config.show_scores then if config.show_scores then
table.insert(items, { entry.score, "TelescopeFrecencyScores" }) table.insert(items, { entry.score, "TelescopeFrecencyScores" })
if config.matcher == "fuzzy" then
table.insert(items, { entry.index, "TelescopeFrecencyScores" })
local score = (not entry.fuzzy_score or entry.fuzzy_score == 0) and "0"
or ("%.3f"):format(entry.fuzzy_score):sub(0, 5)
table.insert(items, { score, "TelescopeFrecencyScores" })
end
end end
if self.web_devicons.is_enabled then if self.web_devicons.is_enabled then
table.insert(items, { self.web_devicons:get_icon(entry.name, entry.name:match "%a+$", { default = true }) }) table.insert(items, { self.web_devicons:get_icon(entry.name, entry.name:match "%a+$", { default = true }) })

View File

@ -0,0 +1,26 @@
local config = require "frecency.config"
local sorters = require "telescope.sorters"
---@param opts any options for get_fzy_sorter()
return function(opts)
local fzy_sorter = sorters.get_fzy_sorter(opts)
return sorters.Sorter:new {
---@param prompt string
---@param entry FrecencyEntry
---@return number
scoring_function = function(_, prompt, _, entry)
if #prompt == 0 then
return 1
end
local fzy_score = fzy_sorter:scoring_function(prompt, entry.ordinal)
if fzy_score <= 0 then
return -1
end
entry.fuzzy_score = config.scoring_function(entry.score, fzy_score)
return entry.fuzzy_score
end,
highlighter = fzy_sorter.highlighter,
}
end

View File

@ -1,6 +1,7 @@
local State = require "frecency.state" local State = require "frecency.state"
local Finder = require "frecency.finder" local Finder = require "frecency.finder"
local config = require "frecency.config" local config = require "frecency.config"
local fuzzy_sorter = require "frecency.fuzzy_sorter"
local sorters = require "telescope.sorters" local sorters = require "telescope.sorters"
local log = require "plenary.log" local log = require "plenary.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
@ -105,7 +106,7 @@ function Picker:start(opts)
prompt_title = "Frecency", prompt_title = "Frecency",
finder = finder, finder = finder,
previewer = config_values.file_previewer(opts), previewer = config_values.file_previewer(opts),
sorter = sorters.get_substr_matcher(), sorter = config.matcher == "default" and sorters.get_substr_matcher() or fuzzy_sorter(opts),
on_input_filter_cb = self:on_input_filter_cb(opts), on_input_filter_cb = self:on_input_filter_cb(opts),
attach_mappings = function(prompt_bufnr) attach_mappings = function(prompt_bufnr)
return self:attach_mappings(prompt_bufnr) return self:attach_mappings(prompt_bufnr)