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: add fuzzy matching sorter experimentally #166

Merged
merged 9 commits into from
Apr 28, 2024
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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`)

Set the max count of timestamps DB keeps when you open files. It ignores the
Expand Down Expand Up @@ -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 the path of the active filter before file paths. In default, it uses the
Expand Down
18 changes: 18 additions & 0 deletions lua/frecency/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ local Config = {}
---@field filter_delimiter string default: ":"
---@field hide_current_buffer boolean default: false
---@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 show_filter_column boolean|string[] default: true
---@field show_scores boolean default: false
Expand All @@ -35,6 +37,7 @@ Config.new = function()
hide_current_buffer = false,
ignore_patterns = os_util.is_windows and { [[*.git\*]], [[*\tmp\*]], "term://*" }
or { "*.git/*", "*/tmp/*", "term://*" },
matcher = "default",
max_timestamps = 10,
recency_values = {
{ age = 240, value = 100 }, -- past 4 hours
Expand All @@ -44,6 +47,12 @@ Config.new = function()
{ age = 43200, value = 20 }, -- past month
{ 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_scores = false,
show_unindexed = true,
Expand All @@ -62,7 +71,9 @@ Config.new = function()
filter_delimiter = true,
hide_current_buffer = true,
ignore_patterns = true,
matcher = true,
max_timestamps = true,
scoring_function = true,
show_filter_column = true,
show_scores = true,
show_unindexed = true,
Expand Down Expand Up @@ -105,6 +116,13 @@ Config.setup = function(ext_config)
filter_delimiter = { opts.filter_delimiter, "s" },
hide_current_buffer = { opts.hide_current_buffer, "b" },
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 = {
opts.max_timestamps,
function(v)
Expand Down
13 changes: 12 additions & 1 deletion lua/frecency/entry_maker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ end
---@field ordinal string
---@field name string
---@field score number
---@field fuzzy_score? number
---@field display fun(entry: FrecencyEntry): string, table

---@class FrecencyFile
Expand Down Expand Up @@ -77,7 +78,11 @@ end
function EntryMaker:displayer_items(workspace, workspace_tag)
local items = {}
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
if self.web_devicons.is_enabled then
table.insert(items, { width = 2 })
Expand All @@ -99,6 +104,12 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
local items = {}
if config.show_scores then
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
if self.web_devicons.is_enabled then
table.insert(items, { self.web_devicons:get_icon(entry.name, entry.name:match "%a+$", { default = true }) })
Expand Down
26 changes: 26 additions & 0 deletions lua/frecency/fuzzy_sorter.lua
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lua/frecency/picker.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local State = require "frecency.state"
local Finder = require "frecency.finder"
local config = require "frecency.config"
local fuzzy_sorter = require "frecency.fuzzy_sorter"
local sorters = require "telescope.sorters"
local log = require "plenary.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
Expand Down Expand Up @@ -105,7 +106,7 @@ function Picker:start(opts)
prompt_title = "Frecency",
finder = finder,
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),
attach_mappings = function(prompt_bufnr)
return self:attach_mappings(prompt_bufnr)
Expand Down
Loading