diff --git a/doc/telescope-frecency.txt b/doc/telescope-frecency.txt index 4444d69..8fc73e5 100644 --- a/doc/telescope-frecency.txt +++ b/doc/telescope-frecency.txt @@ -9,6 +9,7 @@ Requirements |telescope-frecency-requirements| Installation |telescope-frecency-installation| Usage |telescope-frecency-usage| Command |telescope-frecency-command| +Function |telescope-frecency-function| Configuration |telescope-frecency-configuration| Database |telescope-frecency-database| Highlight Groups |telescope-frecency-highlight-groups| @@ -182,6 +183,23 @@ at least, it matches against exact the same as you input. │ `ABC` │ matches `ABC` │ no match │ └──────────────┴───────────────────────┴───────────────────────┘ + *telescope-frecency-combining-results-outside-this-plugin* + *telescope-frecency-live-grep-within-results* +------------------------------------------------------------------------------ +Combining results outside this plugin + +You can use frecency entries even outside this plugin with +|telescope-frecency-function-query|. For example, you can search any input +string within filenames in frecency results. +>lua + vim.keymap.set("n", "tg", function() + local frecency = require("telescope").extensions.frecency + require("telescope.builtin").live_grep { + -- HACK: `search_dirs` can accept files to grep nevertheless its name + search_dirs = frecency.query {}, + } + end, { desc = "Live Grep Frecency" }) + ============================================================================== COMMAND *telescope-frecency-command* @@ -214,6 +232,83 @@ When you set `false` to |telescope-frecency-configuration-db_safe_mode|, the prompts are never shown even if you call without the bang. +============================================================================== +FUNCTION *telescope-frecency-function* + +All functions are exported via `require("telescope").extensions.frecency.*`. +>lua + -- open frecency picker + require("telescope").extensions.frecency.frecency {} +< + *telescope-frecency-function-frecency* +frecency() ~ + +Open the frecency picker. See |telescope-frecency-usage| for examples. + + *telescope-frecency-function-complete* +complete() ~ + +This is used for completing workspace filters in telescope's prompt. Internal +use only. + + *telescope-frecency-function-query* +query() ~ + +Get entries from DB. This is used for combining frecency results outside this +plugin. See |telescope-frecency-combining-results-outside-this-plugin|. +>lua + local frecency = require("telescope").extensions.frecency + local entries = frecency.query {} + -- example results + [ + "/path/to/any/file1.txt", + "/more/path/to/any/file2.txt", + …… + ] + + -- With record = true, it returns other info for each entry. + local records = frecency.query { record = true } + -- example results + [ + { + count = 203, + path = "/path/to/any/file1.txt", + score = 4872, + timestamps = { 1719206250, 1719207356, …… }, + }, + …… + ] + +Options: *telescope-frecency-function-query-options* + *telescope-frecency-function-query-options-direction* + - `direction` type: `"asc"|"desc"` + default: `"desc"` + This specifies the order direction for results. + *telescope-frecency-function-query-options-limit* + - `limit` type: `integer` + default: `100` + This limits the number of results. + *telescope-frecency-function-query-options-order* + - `order` type: `"count"|"path"|"score"|"timestamps"` + default: `"score"` + This is used to sort results with their properties. With + `"count"`, `"score"` and `"timestamps"`, when candidates have the + same value, they will be sorted by `"path"` always ascendingly. + With `"path"`, the order direction differs by the value of + `direction`. + *telescope-frecency-function-query-options-record* + - `record` type: `boolean` + default: `false` + If `false`, it returns results containing filenames with + absolute paths. If `true`, it contains tables with this + properties below. + + `count`: Count that the file has been opened. + `path`: Absolute path for the file. + `score`: Recency score to be used in frecency picker. + `timestamps`: UNIX timestamps that the file has been opened at. + + ============================================================================== CONFIGURATION *telescope-frecency-configuration* diff --git a/lua/frecency/database.lua b/lua/frecency/database.lua index 27c5bf4..589c34b 100644 --- a/lua/frecency/database.lua +++ b/lua/frecency/database.lua @@ -11,6 +11,7 @@ local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] ---@field count integer ---@field path string ---@field score number +---@field timestamps integer[] ---@class FrecencyDatabase ---@field tx FrecencyPlenaryAsyncControlChannelTx @@ -105,11 +106,11 @@ function Database:remove_files(paths) end ---@param path string ----@param datetime? string -function Database:update(path, datetime) +---@param epoch? integer +function Database:update(path, epoch) local record = self.tbl.records[path] or { count = 0, timestamps = {} } record.count = record.count + 1 - local now = self:now(datetime) + local now = epoch or os.time() table.insert(record.timestamps, now) if #record.timestamps > config.max_timestamps then local new_table = {} @@ -123,10 +124,10 @@ function Database:update(path, datetime) end ---@param workspace? string ----@param datetime? string +---@param epoch? integer ---@return FrecencyDatabaseEntry[] -function Database:get_entries(workspace, datetime) - local now = self:now(datetime) +function Database:get_entries(workspace, epoch) + local now = epoch or os.time() local items = {} for path, record in pairs(self.tbl.records) do if self.fs:starts_with(path, workspace) then @@ -136,25 +137,13 @@ function Database:get_entries(workspace, datetime) ages = vim.tbl_map(function(v) return (now - v) / 60 end, record.timestamps), + timestamps = record.timestamps, }) end end return items end --- TODO: remove this func --- This is a func for testing ----@private ----@param datetime string? ----@return integer -function Database:now(datetime) - if not datetime then - return os.time() - end - local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2") - return require("frecency.tests.util").time_piece(tz_fix) -end - ---@async ---@return nil function Database:load() diff --git a/lua/frecency/database/table.lua b/lua/frecency/database/table.lua index 79a4945..667bf58 100644 --- a/lua/frecency/database/table.lua +++ b/lua/frecency/database/table.lua @@ -1,12 +1,12 @@ local log = require "plenary.log" ----@class FrecencyDatabaseRecord +---@class FrecencyDatabaseRecordValue ---@field count integer ---@field timestamps integer[] ---@class FrecencyDatabaseRawTable ---@field version string ----@field records table +---@field records table ---@class FrecencyDatabaseTable: FrecencyDatabaseRawTable ---@field private is_ready boolean diff --git a/lua/frecency/finder.lua b/lua/frecency/finder.lua index 20ba9bb..79f2ca5 100644 --- a/lua/frecency/finder.lua +++ b/lua/frecency/finder.lua @@ -76,9 +76,9 @@ Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, st return self end ----@param datetime? string +---@param epoch? integer ---@return nil -function Finder:start(datetime) +function Finder:start(epoch) local ok if config.workspace_scan_cmd ~= "LUA" and self.need_scan_dir then ---@type string[][] @@ -95,7 +95,7 @@ function Finder:start(datetime) async.void(function() -- NOTE: return to the main loop to show the main window async.util.scheduler() - for _, file in ipairs(self:get_results(self.path, datetime)) do + for _, file in ipairs(self:get_results(self.path, epoch)) do file.path = os_util.normalize_sep(file.path) local entry = self.entry_maker(file) self.tx.send(entry) @@ -255,12 +255,12 @@ function Finder:process_channel(process_result, entries, rx, start_index) end ---@param workspace? string ----@param datetime? string +---@param epoch? integer ---@return FrecencyFile[] -function Finder:get_results(workspace, datetime) +function Finder:get_results(workspace, epoch) log.debug { workspace = workspace or "NONE" } local start_fetch = os.clock() - local files = self.database:get_entries(workspace, datetime) + local files = self.database:get_entries(workspace, epoch) log.debug(("it takes %f seconds in fetching entries"):format(os.clock() - start_fetch)) local start_results = os.clock() local elapsed_recency = 0 diff --git a/lua/frecency/init.lua b/lua/frecency/init.lua index 8397c83..c536c63 100644 --- a/lua/frecency/init.lua +++ b/lua/frecency/init.lua @@ -107,13 +107,13 @@ function Frecency:validate_database(force) end ---@param bufnr integer ----@param datetime? string ISO8601 format string -function Frecency:register(bufnr, datetime) +---@param epoch? integer +function Frecency:register(bufnr, epoch) local path = vim.api.nvim_buf_get_name(bufnr) if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then return end - self.database:update(path, datetime) + self.database:update(path, epoch) self.buf_registered[bufnr] = true end @@ -127,6 +127,98 @@ function Frecency:delete(path) end end +---@alias FrecencyQueryOrder "count"|"path"|"score"|"timestamps" +---@alias FrecencyQueryDirection "asc"|"desc" + +---@class FrecencyQueryOpts +---@field direction? "asc"|"desc" default: "desc" +---@field limit? integer default: 100 +---@field order? FrecencyQueryOrder default: "score" +---@field record? boolean default: false +---@field workspace? string default: nil + +---@class FrecencyQueryEntry +---@field count integer +---@field path string +---@field score number +---@field timestamps integer[] + +---@param opts? FrecencyQueryOpts +---@param epoch? integer +---@return FrecencyQueryEntry[]|string[] +function Frecency:query(opts, epoch) + opts = vim.tbl_extend("force", { + direction = "desc", + limit = 100, + order = "score", + record = false, + }, opts or {}) + ---@param entry FrecencyDatabaseEntry + local entries = vim.tbl_map(function(entry) + return { + count = entry.count, + path = entry.path, + score = entry.ages and self.recency:calculate(entry.count, entry.ages) or 0, + timestamps = entry.timestamps, + } + end, self.database:get_entries(opts.workspace, epoch)) + table.sort(entries, self:query_sorter(opts.order, opts.direction)) + local results = opts.record and entries or vim.tbl_map(function(entry) + return entry.path + end, entries) + if #results > opts.limit then + return vim.list_slice(results, 1, opts.limit) + end + return results +end + +---@private +---@param order FrecencyQueryOrder +---@param direction FrecencyQueryDirection +---@return fun(a: FrecencyQueryEntry, b: FrecencyQueryEntry): boolean +function Frecency:query_sorter(order, direction) + local is_asc = direction == "asc" + if order == "count" then + if is_asc then + return function(a, b) + return a.count < b.count or (a.count == b.count and a.path < b.path) + end + end + return function(a, b) + return a.count > b.count or (a.count == b.count and a.path < b.path) + end + elseif order == "path" then + if is_asc then + return function(a, b) + return a.path < b.path + end + end + return function(a, b) + return a.path > b.path + end + elseif order == "score" then + if is_asc then + return function(a, b) + return a.score < b.score or (a.score == b.score and a.path < b.path) + end + end + return function(a, b) + return a.score > b.score or (a.score == b.score and a.path < b.path) + end + elseif is_asc then + return function(a, b) + local a_timestamp = a.timestamps[1] or 0 + local b_timestamp = b.timestamps[1] or 0 + return a_timestamp < b_timestamp or (a_timestamp == b_timestamp and a.path < b.path) + end + end + return function(a, b) + local a_timestamp = a.timestamps[1] or 0 + local b_timestamp = b.timestamps[1] or 0 + return a_timestamp > b_timestamp or (a_timestamp == b_timestamp and a.path < b.path) + end +end + ---@private ---@param fmt string ---@param ...? any diff --git a/lua/frecency/tests/database_spec.lua b/lua/frecency/tests/database_spec.lua index 204a06c..f205990 100644 --- a/lua/frecency/tests/database_spec.lua +++ b/lua/frecency/tests/database_spec.lua @@ -5,6 +5,16 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local util = require "frecency.tests.util" async.tests.add_to_env() +---@param datetime string? +---@return integer +local function make_epoch(datetime) + if not datetime then + return os.time() + end + local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2") + return util.time_piece(tz_fix) +end + local function with_database(f) local fs = FS.new { ignore_patterns = {} } local dir, close = util.tmpdir() @@ -17,10 +27,15 @@ local function with_database(f) end end -local function save_and_load(database, tbl, datetime) +---@async +---@param database FrecencyDatabase +---@param tbl table +---@param epoch integer +---@return FrecencyEntry[] +local function save_and_load(database, tbl, epoch) database:raw_save(util.v1_table(tbl)) async.util.sleep(100) - local entries = database:get_entries(nil, datetime) + local entries = database:get_entries(nil, epoch) table.sort(entries, function(a, b) return a.path < b.path end) @@ -31,16 +46,27 @@ a.describe("frecency.database", function() a.describe("updated by another process", function() a.it( "returns valid entries", + ---@param database FrecencyDatabase with_database(function(database) assert.are.same( { - { path = "hoge1.txt", count = 1, ages = { 60 } }, - { path = "hoge2.txt", count = 1, ages = { 60 } }, + { + path = "hoge1.txt", + count = 1, + ages = { 60 }, + timestamps = { make_epoch "2023-08-21T00:00:00+09:00" }, + }, + { + path = "hoge2.txt", + count = 1, + ages = { 60 }, + timestamps = { make_epoch "2023-08-21T00:00:00+09:00" }, + }, }, save_and_load(database, { - ["hoge1.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } }, - ["hoge2.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } }, - }, "2023-08-21T01:00:00+0000") + ["hoge1.txt"] = { count = 1, timestamps = { make_epoch "2023-08-21T00:00:00+09:00" } }, + ["hoge2.txt"] = { count = 1, timestamps = { make_epoch "2023-08-21T00:00:00+09:00" } }, + }, make_epoch "2023-08-21T01:00:00+09:00") ) end) ) diff --git a/lua/frecency/tests/frecency_spec.lua b/lua/frecency/tests/frecency_spec.lua index f201262..25e276a 100644 --- a/lua/frecency/tests/frecency_spec.lua +++ b/lua/frecency/tests/frecency_spec.lua @@ -10,6 +10,16 @@ local log = require "plenary.log" local Path = require "plenary.path" local config = require "frecency.config" +---@param datetime string? +---@return integer +local function make_epoch(datetime) + if not datetime then + return os.time() + end + local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2") + return util.time_piece(tz_fix) +end + ---@param files string[] ---@param cb_or_config table|fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil ---@param callback? fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil @@ -28,13 +38,8 @@ local function with_files(files, cb_or_config, callback) config.setup(cfg) local frecency = Frecency.new() frecency.database.tbl:wait_ready() - frecency.picker = Picker.new( - frecency.database, - frecency.entry_maker, - frecency.fs, - frecency.recency, - { editing_bufnr = 0 } - ) + frecency.picker = + Picker.new(frecency.database, frecency.entry_maker, frecency.fs, frecency.recency, { editing_bufnr = 0 }) local finder = frecency.picker:finder {} callback(frecency, finder, dir) close() @@ -46,22 +51,22 @@ end ---@param frecency Frecency ---@param dir FrecencyPlenaryPath ----@return fun(file: string, datetime: string, reset: boolean?): nil +---@return fun(file: string, epoch: integer, reset: boolean?): nil local function make_register(frecency, dir) - return function(file, datetime, reset) + return function(file, epoch, reset) local path = filepath(dir, file) vim.cmd.edit(path) local bufnr = assert(vim.fn.bufnr(path)) if reset then frecency.buf_registered[bufnr] = nil end - frecency:register(bufnr, datetime) + frecency:register(bufnr, epoch) end end ---@param frecency Frecency ---@param dir FrecencyPlenaryPath ----@param callback fun(register: fun(file: string, datetime: string?): nil): nil +---@param callback fun(register: fun(file: string, epoch?: integer): nil): nil ---@return nil local function with_fake_register(frecency, dir, callback) local bufnr = 0 @@ -71,12 +76,14 @@ local function with_fake_register(frecency, dir, callback) vim.api.nvim_buf_get_name = function(bufnr) return buffers[bufnr] end - local function register(file, datetime) + ---@param file string + ---@param epoch integer + local function register(file, epoch) local path = filepath(dir, file) Path.new(path):touch() bufnr = bufnr + 1 buffers[bufnr] = path - frecency:register(bufnr, datetime) + frecency:register(bufnr, epoch) end callback(register) vim.api.nvim_buf_get_name = original_nvim_buf_get_name @@ -107,14 +114,16 @@ describe("frecency", function() describe("when opening files", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T01:00:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T01:00:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) it("has valid records in DB", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } }, }, results) end) end) @@ -123,15 +132,18 @@ describe("frecency", function() describe("when opening again", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T01:00:00+09:00") - register("hoge1.txt", "2023-07-29T02:00:00+09:00", true) + local epoch11 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T01:00:00+09:00" + local epoch12 = make_epoch "2023-07-29T02:00:00+09:00" + register("hoge1.txt", epoch11) + register("hoge2.txt", epoch2) + register("hoge1.txt", epoch12, true) it("increases the score", function() - local results = finder:get_results(nil, "2023-07-29T03:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T03:00:00+09:00") assert.are.same({ - { count = 2, path = filepath(dir, "hoge1.txt"), score = 40 }, - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, + { count = 2, path = filepath(dir, "hoge1.txt"), score = 40, timestamps = { epoch11, epoch12 } }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, }, results) end) end) @@ -140,15 +152,18 @@ describe("frecency", function() describe("when opening again but the same instance", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T01:00:00+09:00") - register("hoge1.txt", "2023-07-29T02:00:00+09:00") + local epoch11 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T01:00:00+09:00" + local epoch12 = make_epoch "2023-07-29T02:00:00+09:00" + register("hoge1.txt", epoch11) + register("hoge2.txt", epoch2) + register("hoge1.txt", epoch12) it("does not increase the score", function() - local results = finder:get_results(nil, "2023-07-29T03:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T03:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch11 } }, }, results) end) end) @@ -157,27 +172,62 @@ describe("frecency", function() describe("when opening more than 10 times", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge1.txt", "2023-07-29T00:01:00+09:00", true) + local epoch11 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch12 = make_epoch "2023-07-29T00:01:00+09:00" + register("hoge1.txt", epoch11) + register("hoge1.txt", epoch12, true) - register("hoge2.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:02:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:03:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:04:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:05:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:06:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:07:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:08:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:09:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:10:00+09:00", true) - register("hoge2.txt", "2023-07-29T00:11:00+09:00", true) + local epoch201 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch202 = make_epoch "2023-07-29T00:01:00+09:00" + local epoch203 = make_epoch "2023-07-29T00:02:00+09:00" + local epoch204 = make_epoch "2023-07-29T00:03:00+09:00" + local epoch205 = make_epoch "2023-07-29T00:04:00+09:00" + local epoch206 = make_epoch "2023-07-29T00:05:00+09:00" + local epoch207 = make_epoch "2023-07-29T00:06:00+09:00" + local epoch208 = make_epoch "2023-07-29T00:07:00+09:00" + local epoch209 = make_epoch "2023-07-29T00:08:00+09:00" + local epoch210 = make_epoch "2023-07-29T00:09:00+09:00" + local epoch211 = make_epoch "2023-07-29T00:10:00+09:00" + local epoch212 = make_epoch "2023-07-29T00:11:00+09:00" + register("hoge2.txt", epoch201) + register("hoge2.txt", epoch202, true) + register("hoge2.txt", epoch203, true) + register("hoge2.txt", epoch204, true) + register("hoge2.txt", epoch205, true) + register("hoge2.txt", epoch206, true) + register("hoge2.txt", epoch207, true) + register("hoge2.txt", epoch208, true) + register("hoge2.txt", epoch209, true) + register("hoge2.txt", epoch210, true) + register("hoge2.txt", epoch211, true) + register("hoge2.txt", epoch212, true) it("calculates score from the recent 10 times", function() - local results = finder:get_results(nil, "2023-07-29T00:12:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T00:12:00+09:00") assert.are.same({ - { count = 12, path = filepath(dir, "hoge2.txt"), score = 12 * (10 * 100) / 10 }, - { count = 2, path = filepath(dir, "hoge1.txt"), score = 2 * (2 * 100) / 10 }, + { + count = 12, + path = filepath(dir, "hoge2.txt"), + score = 12 * (10 * 100) / 10, + timestamps = { + epoch203, + epoch204, + epoch205, + epoch206, + epoch207, + epoch208, + epoch209, + epoch210, + epoch211, + epoch212, + }, + }, + { + count = 2, + path = filepath(dir, "hoge1.txt"), + score = 2 * (2 * 100) / 10, + timestamps = { epoch11, epoch12 }, + }, }, results) end) end) @@ -202,11 +252,14 @@ describe("frecency", function() table.insert(expected, { count = 1, path = filepath(dir, file), score = 10 }) -- HACK: disable log because it fails with too many logging log.new({ level = "info" }, true) - register(file, "2023-07-29T00:00:00+09:00") + register(file, make_epoch "2023-07-29T00:00:00+09:00") log.new({}, true) end local start = os.clock() - local results = finder:get_results(nil, "2023-07-29T00:01:00+09:00") + local results = vim.tbl_map(function(result) + result.timestamps = nil + return result + end, finder:get_results(nil, make_epoch "2023-07-29T00:01:00+09:00")) table.sort(results, function(a, b) return a.path < b.path end) @@ -229,14 +282,16 @@ describe("frecency", function() describe("when no files are unlinked", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) it("removes no entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } }, }, results) end) end) @@ -249,26 +304,31 @@ describe("frecency", function() { db_validate_threshold = 3 }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") - register("hoge3.txt", "2023-07-29T00:02:00+09:00") - register("hoge4.txt", "2023-07-29T00:03:00+09:00") - register("hoge5.txt", "2023-07-29T00:04:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + local epoch3 = make_epoch "2023-07-29T00:02:00+09:00" + local epoch4 = make_epoch "2023-07-29T00:03:00+09:00" + local epoch5 = make_epoch "2023-07-29T00:04:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) + register("hoge3.txt", epoch3) + register("hoge4.txt", epoch4) + register("hoge5.txt", epoch5) dir:joinpath("hoge1.txt"):rm() dir:joinpath("hoge2.txt"):rm() frecency:validate_database() it("removes no entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") table.sort(results, function(a, b) return a.path < b.path end) assert.are.same({ - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 1, path = filepath(dir, "hoge3.txt"), score = 10, timestamps = { epoch3 } }, + { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } }, + { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } }, }, results) end) end @@ -282,11 +342,16 @@ describe("frecency", function() { db_validate_threshold = 3 }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") - register("hoge3.txt", "2023-07-29T00:02:00+09:00") - register("hoge4.txt", "2023-07-29T00:03:00+09:00") - register("hoge5.txt", "2023-07-29T00:04:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + local epoch3 = make_epoch "2023-07-29T00:02:00+09:00" + local epoch4 = make_epoch "2023-07-29T00:03:00+09:00" + local epoch5 = make_epoch "2023-07-29T00:04:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) + register("hoge3.txt", epoch3) + register("hoge4.txt", epoch4) + register("hoge5.txt", epoch5) dir:joinpath("hoge1.txt"):rm() dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge3.txt"):rm() @@ -300,13 +365,13 @@ describe("frecency", function() end) it("removes entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") table.sort(results, function(a, b) return a.path < b.path end) assert.are.same({ - { count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } }, + { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } }, }, results) end) end @@ -319,11 +384,16 @@ describe("frecency", function() { db_validate_threshold = 3 }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") - register("hoge3.txt", "2023-07-29T00:02:00+09:00") - register("hoge4.txt", "2023-07-29T00:03:00+09:00") - register("hoge5.txt", "2023-07-29T00:04:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + local epoch3 = make_epoch "2023-07-29T00:02:00+09:00" + local epoch4 = make_epoch "2023-07-29T00:03:00+09:00" + local epoch5 = make_epoch "2023-07-29T00:04:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) + register("hoge3.txt", epoch3) + register("hoge4.txt", epoch4) + register("hoge5.txt", epoch5) dir:joinpath("hoge1.txt"):rm() dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge3.txt"):rm() @@ -337,16 +407,16 @@ describe("frecency", function() end) it("removes no entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") table.sort(results, function(a, b) return a.path < b.path end) assert.are.same({ - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, - { count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 1, path = filepath(dir, "hoge3.txt"), score = 10, timestamps = { epoch3 } }, + { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } }, + { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } }, }, results) end) end @@ -359,8 +429,10 @@ describe("frecency", function() describe("when db_safe_mode is true", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) dir:joinpath("hoge1.txt"):rm() with_fake_vim_ui_select("y", function(called) @@ -372,9 +444,9 @@ describe("frecency", function() end) it("needs confirmation for removing entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, }, results) end) end) @@ -383,8 +455,10 @@ describe("frecency", function() describe("when db_safe_mode is false", function() with_files({ "hoge1.txt", "hoge2.txt" }, { db_safe_mode = false }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) dir:joinpath("hoge1.txt"):rm() with_fake_vim_ui_select("y", function(called) @@ -396,9 +470,9 @@ describe("frecency", function() end) it("needs no confirmation for removing entries", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, }, results) end) end) @@ -410,8 +484,10 @@ describe("frecency", function() describe("when file exists", function() with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) local register = make_register(frecency, dir) - register("hoge1.txt", "2023-07-29T00:00:00+09:00") - register("hoge2.txt", "2023-07-29T00:01:00+09:00") + local epoch1 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + register("hoge1.txt", epoch1) + register("hoge2.txt", epoch2) it("deletes the file successfully", function() local path = filepath(dir, "hoge2.txt") @@ -426,12 +502,97 @@ describe("frecency", function() end) it("returns valid results", function() - local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00") + local results = finder:get_results(nil, make_epoch "2023-07-29T02:00:00+09:00") assert.are.same({ - { count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, + { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } }, }, results) end) end) end) end) + + describe("query", function() + with_files({ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt" }, function(frecency, _, dir) + local register = make_register(frecency, dir) + local epoch11 = make_epoch "2023-07-29T00:00:00+09:00" + local epoch2 = make_epoch "2023-07-29T00:01:00+09:00" + local epoch12 = make_epoch "2023-07-29T00:02:00+09:00" + local epoch31 = make_epoch "2023-07-29T00:03:00+09:00" + local epoch13 = make_epoch "2023-07-29T00:04:00+09:00" + local epoch32 = make_epoch "2023-07-29T00:05:00+09:00" + local epoch4 = make_epoch "2023-07-29T00:06:00+09:00" + register("hoge1.txt", epoch11) + register("hoge2.txt", epoch2) + register("hoge1.txt", epoch12, true) + register("hoge3.txt", epoch31) + register("hoge1.txt", epoch13, true) + register("hoge3.txt", epoch32, true) + register("hoge4.txt", epoch4) + + for _, c in ipairs { + { + desc = "with no opts", + opts = nil, + results = { + filepath(dir, "hoge1.txt"), + filepath(dir, "hoge3.txt"), + filepath(dir, "hoge2.txt"), + filepath(dir, "hoge4.txt"), + }, + }, + { + desc = "with an empty opts", + opts = {}, + results = { + filepath(dir, "hoge1.txt"), + filepath(dir, "hoge3.txt"), + filepath(dir, "hoge2.txt"), + filepath(dir, "hoge4.txt"), + }, + }, + { + desc = "with limit", + opts = { limit = 3 }, + results = { + filepath(dir, "hoge1.txt"), + filepath(dir, "hoge3.txt"), + filepath(dir, "hoge2.txt"), + }, + }, + { + desc = "with limit, direction", + opts = { direction = "asc", limit = 3 }, + results = { + filepath(dir, "hoge2.txt"), + filepath(dir, "hoge4.txt"), + filepath(dir, "hoge3.txt"), + }, + }, + { + desc = "with limit, direction, order", + opts = { direction = "asc", limit = 3, order = "path" }, + results = { + filepath(dir, "hoge1.txt"), + filepath(dir, "hoge2.txt"), + filepath(dir, "hoge3.txt"), + }, + }, + { + desc = "with limit, direction, order, record", + opts = { direction = "asc", limit = 3, order = "path", record = true }, + results = { + { count = 3, path = filepath(dir, "hoge1.txt"), score = 90, timestamps = { epoch11, epoch12, epoch13 } }, + { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } }, + { count = 2, path = filepath(dir, "hoge3.txt"), score = 40, timestamps = { epoch31, epoch32 } }, + }, + }, + } do + describe(c.desc, function() + it("returns valid results", function() + assert.are.same(c.results, frecency:query(c.opts, make_epoch "2023-07-29T04:00:00+09:00")) + end) + end) + end + end) + end) end) diff --git a/lua/frecency/tests/util.lua b/lua/frecency/tests/util.lua index ca42522..b7c230e 100644 --- a/lua/frecency/tests/util.lua +++ b/lua/frecency/tests/util.lua @@ -47,16 +47,8 @@ local function time_piece(iso8601) return epoch end ----@param source table -local function v1_table(source) - local records = {} - for path, record in pairs(source) do - local timestamps = {} - for _, iso8601 in ipairs(record.timestamps) do - table.insert(timestamps, time_piece(iso8601)) - end - records[path] = { count = record.count, timestamps = timestamps } - end +---@param records table +local function v1_table(records) return { version = "v1", records = records } end diff --git a/lua/telescope/_extensions/frecency.lua b/lua/telescope/_extensions/frecency.lua index fecb56c..728f4c3 100644 --- a/lua/telescope/_extensions/frecency.lua +++ b/lua/telescope/_extensions/frecency.lua @@ -4,6 +4,7 @@ ---@class FrecencyInstance ---@field complete fun(findstart: 1|0, base: string): integer|''|string[] ---@field delete fun(path: string): nil +---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[] ---@field register fun(bufnr: integer, datetime: string?): nil ---@field start fun(opts: FrecencyPickerOptions?): nil ---@field validate_database fun(force: boolean?): nil @@ -31,6 +32,7 @@ return require("telescope").register_extension { exports = { frecency = frecency.start, complete = frecency.complete, + query = frecency.query, }, ---When this func is called, Frecency instance is NOT created but only