diff --git a/README.md b/README.md index 8c5434f6..a94bd49d 100644 --- a/README.md +++ b/README.md @@ -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___
+ > 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 @@ -245,6 +261,27 @@ See [default configuration](https://github.com/nvim-telescope/telescope.nvim#tel } ``` +- `scoring_function` (default: see below) + + > ___CAUTION___
+ > 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 diff --git a/lua/frecency/config.lua b/lua/frecency/config.lua index 7e2de920..eb2d3f40 100644 --- a/lua/frecency/config.lua +++ b/lua/frecency/config.lua @@ -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 @@ -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 @@ -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, @@ -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, @@ -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) diff --git a/lua/frecency/entry_maker.lua b/lua/frecency/entry_maker.lua index 91a4211c..0a3884e9 100644 --- a/lua/frecency/entry_maker.lua +++ b/lua/frecency/entry_maker.lua @@ -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 @@ -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 }) @@ -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 }) }) diff --git a/lua/frecency/fuzzy_sorter.lua b/lua/frecency/fuzzy_sorter.lua new file mode 100644 index 00000000..b4db10b1 --- /dev/null +++ b/lua/frecency/fuzzy_sorter.lua @@ -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 diff --git a/lua/frecency/picker.lua b/lua/frecency/picker.lua index e2a6e918..a14832f2 100644 --- a/lua/frecency/picker.lua +++ b/lua/frecency/picker.lua @@ -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]] @@ -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)