feat: add frecency.query() to query DB (#217)

* feat: add function to query the DB

* docs: add documentation for frecency.query()

* test: fix tests to run with `timestamps` property

* test: add tests for frecency.query()
This commit is contained in:
JINNOUCHI Yasushi 2024-07-06 15:35:59 +09:00 committed by GitHub
parent f3f9325379
commit 8f593064f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 500 additions and 143 deletions

View File

@ -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", "<Leader>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*

View File

@ -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()

View File

@ -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<string,FrecencyDatabaseRecord>
---@field records table<string,FrecencyDatabaseRecordValue>
---@class FrecencyDatabaseTable: FrecencyDatabaseRawTable
---@field private is_ready boolean

View File

@ -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

View File

@ -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

View File

@ -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<string, FrecencyDatabaseRecordValue>
---@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)
)

View File

@ -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)

View File

@ -47,16 +47,8 @@ local function time_piece(iso8601)
return epoch
end
---@param source table<string,{ count: integer, timestamps: string[] }>
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<string, FrecencyDatabaseRecordValue>
local function v1_table(records)
return { version = "v1", records = records }
end

View File

@ -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