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| Installation |telescope-frecency-installation|
Usage |telescope-frecency-usage| Usage |telescope-frecency-usage|
Command |telescope-frecency-command| Command |telescope-frecency-command|
Function |telescope-frecency-function|
Configuration |telescope-frecency-configuration| Configuration |telescope-frecency-configuration|
Database |telescope-frecency-database| Database |telescope-frecency-database|
Highlight Groups |telescope-frecency-highlight-groups| 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 │ │ `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* 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. 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* CONFIGURATION *telescope-frecency-configuration*

View File

@ -11,6 +11,7 @@ local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
---@field count integer ---@field count integer
---@field path string ---@field path string
---@field score number ---@field score number
---@field timestamps integer[]
---@class FrecencyDatabase ---@class FrecencyDatabase
---@field tx FrecencyPlenaryAsyncControlChannelTx ---@field tx FrecencyPlenaryAsyncControlChannelTx
@ -105,11 +106,11 @@ function Database:remove_files(paths)
end end
---@param path string ---@param path string
---@param datetime? string ---@param epoch? integer
function Database:update(path, datetime) function Database:update(path, epoch)
local record = self.tbl.records[path] or { count = 0, timestamps = {} } local record = self.tbl.records[path] or { count = 0, timestamps = {} }
record.count = record.count + 1 record.count = record.count + 1
local now = self:now(datetime) local now = epoch or os.time()
table.insert(record.timestamps, now) table.insert(record.timestamps, now)
if #record.timestamps > config.max_timestamps then if #record.timestamps > config.max_timestamps then
local new_table = {} local new_table = {}
@ -123,10 +124,10 @@ function Database:update(path, datetime)
end end
---@param workspace? string ---@param workspace? string
---@param datetime? string ---@param epoch? integer
---@return FrecencyDatabaseEntry[] ---@return FrecencyDatabaseEntry[]
function Database:get_entries(workspace, datetime) function Database:get_entries(workspace, epoch)
local now = self:now(datetime) local now = epoch or os.time()
local items = {} local items = {}
for path, record in pairs(self.tbl.records) do for path, record in pairs(self.tbl.records) do
if self.fs:starts_with(path, workspace) then if self.fs:starts_with(path, workspace) then
@ -136,25 +137,13 @@ function Database:get_entries(workspace, datetime)
ages = vim.tbl_map(function(v) ages = vim.tbl_map(function(v)
return (now - v) / 60 return (now - v) / 60
end, record.timestamps), end, record.timestamps),
timestamps = record.timestamps,
}) })
end end
end end
return items return items
end 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 ---@async
---@return nil ---@return nil
function Database:load() function Database:load()

View File

@ -1,12 +1,12 @@
local log = require "plenary.log" local log = require "plenary.log"
---@class FrecencyDatabaseRecord ---@class FrecencyDatabaseRecordValue
---@field count integer ---@field count integer
---@field timestamps integer[] ---@field timestamps integer[]
---@class FrecencyDatabaseRawTable ---@class FrecencyDatabaseRawTable
---@field version string ---@field version string
---@field records table<string,FrecencyDatabaseRecord> ---@field records table<string,FrecencyDatabaseRecordValue>
---@class FrecencyDatabaseTable: FrecencyDatabaseRawTable ---@class FrecencyDatabaseTable: FrecencyDatabaseRawTable
---@field private is_ready boolean ---@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 return self
end end
---@param datetime? string ---@param epoch? integer
---@return nil ---@return nil
function Finder:start(datetime) function Finder:start(epoch)
local ok local ok
if config.workspace_scan_cmd ~= "LUA" and self.need_scan_dir then if config.workspace_scan_cmd ~= "LUA" and self.need_scan_dir then
---@type string[][] ---@type string[][]
@ -95,7 +95,7 @@ function Finder:start(datetime)
async.void(function() async.void(function()
-- NOTE: return to the main loop to show the main window -- NOTE: return to the main loop to show the main window
async.util.scheduler() 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) file.path = os_util.normalize_sep(file.path)
local entry = self.entry_maker(file) local entry = self.entry_maker(file)
self.tx.send(entry) self.tx.send(entry)
@ -255,12 +255,12 @@ function Finder:process_channel(process_result, entries, rx, start_index)
end end
---@param workspace? string ---@param workspace? string
---@param datetime? string ---@param epoch? integer
---@return FrecencyFile[] ---@return FrecencyFile[]
function Finder:get_results(workspace, datetime) function Finder:get_results(workspace, epoch)
log.debug { workspace = workspace or "NONE" } log.debug { workspace = workspace or "NONE" }
local start_fetch = os.clock() 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)) log.debug(("it takes %f seconds in fetching entries"):format(os.clock() - start_fetch))
local start_results = os.clock() local start_results = os.clock()
local elapsed_recency = 0 local elapsed_recency = 0

View File

@ -107,13 +107,13 @@ function Frecency:validate_database(force)
end end
---@param bufnr integer ---@param bufnr integer
---@param datetime? string ISO8601 format string ---@param epoch? integer
function Frecency:register(bufnr, datetime) function Frecency:register(bufnr, epoch)
local path = vim.api.nvim_buf_get_name(bufnr) local path = vim.api.nvim_buf_get_name(bufnr)
if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then
return return
end end
self.database:update(path, datetime) self.database:update(path, epoch)
self.buf_registered[bufnr] = true self.buf_registered[bufnr] = true
end end
@ -127,6 +127,98 @@ function Frecency:delete(path)
end end
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 ---@private
---@param fmt string ---@param fmt string
---@param ...? any ---@param ...? any

View File

@ -5,6 +5,16 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local util = require "frecency.tests.util" local util = require "frecency.tests.util"
async.tests.add_to_env() 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 function with_database(f)
local fs = FS.new { ignore_patterns = {} } local fs = FS.new { ignore_patterns = {} }
local dir, close = util.tmpdir() local dir, close = util.tmpdir()
@ -17,10 +27,15 @@ local function with_database(f)
end end
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)) database:raw_save(util.v1_table(tbl))
async.util.sleep(100) async.util.sleep(100)
local entries = database:get_entries(nil, datetime) local entries = database:get_entries(nil, epoch)
table.sort(entries, function(a, b) table.sort(entries, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
@ -31,16 +46,27 @@ a.describe("frecency.database", function()
a.describe("updated by another process", function() a.describe("updated by another process", function()
a.it( a.it(
"returns valid entries", "returns valid entries",
---@param database FrecencyDatabase
with_database(function(database) with_database(function(database)
assert.are.same( 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, { save_and_load(database, {
["hoge1.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } }, ["hoge1.txt"] = { count = 1, timestamps = { make_epoch "2023-08-21T00:00:00+09:00" } },
["hoge2.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } }, ["hoge2.txt"] = { count = 1, timestamps = { make_epoch "2023-08-21T00:00:00+09:00" } },
}, "2023-08-21T01:00:00+0000") }, make_epoch "2023-08-21T01:00:00+09:00")
) )
end) end)
) )

View File

@ -10,6 +10,16 @@ local log = require "plenary.log"
local Path = require "plenary.path" local Path = require "plenary.path"
local config = require "frecency.config" 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 files string[]
---@param cb_or_config table|fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil ---@param cb_or_config table|fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil
---@param callback? 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) config.setup(cfg)
local frecency = Frecency.new() local frecency = Frecency.new()
frecency.database.tbl:wait_ready() frecency.database.tbl:wait_ready()
frecency.picker = Picker.new( frecency.picker =
frecency.database, Picker.new(frecency.database, frecency.entry_maker, frecency.fs, frecency.recency, { editing_bufnr = 0 })
frecency.entry_maker,
frecency.fs,
frecency.recency,
{ editing_bufnr = 0 }
)
local finder = frecency.picker:finder {} local finder = frecency.picker:finder {}
callback(frecency, finder, dir) callback(frecency, finder, dir)
close() close()
@ -46,22 +51,22 @@ end
---@param frecency Frecency ---@param frecency Frecency
---@param dir FrecencyPlenaryPath ---@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) local function make_register(frecency, dir)
return function(file, datetime, reset) return function(file, epoch, reset)
local path = filepath(dir, file) local path = filepath(dir, file)
vim.cmd.edit(path) vim.cmd.edit(path)
local bufnr = assert(vim.fn.bufnr(path)) local bufnr = assert(vim.fn.bufnr(path))
if reset then if reset then
frecency.buf_registered[bufnr] = nil frecency.buf_registered[bufnr] = nil
end end
frecency:register(bufnr, datetime) frecency:register(bufnr, epoch)
end end
end end
---@param frecency Frecency ---@param frecency Frecency
---@param dir FrecencyPlenaryPath ---@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 ---@return nil
local function with_fake_register(frecency, dir, callback) local function with_fake_register(frecency, dir, callback)
local bufnr = 0 local bufnr = 0
@ -71,12 +76,14 @@ local function with_fake_register(frecency, dir, callback)
vim.api.nvim_buf_get_name = function(bufnr) vim.api.nvim_buf_get_name = function(bufnr)
return buffers[bufnr] return buffers[bufnr]
end end
local function register(file, datetime) ---@param file string
---@param epoch integer
local function register(file, epoch)
local path = filepath(dir, file) local path = filepath(dir, file)
Path.new(path):touch() Path.new(path):touch()
bufnr = bufnr + 1 bufnr = bufnr + 1
buffers[bufnr] = path buffers[bufnr] = path
frecency:register(bufnr, datetime) frecency:register(bufnr, epoch)
end end
callback(register) callback(register)
vim.api.nvim_buf_get_name = original_nvim_buf_get_name vim.api.nvim_buf_get_name = original_nvim_buf_get_name
@ -107,14 +114,16 @@ describe("frecency", function()
describe("when opening files", function() describe("when opening files", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T01: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() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } },
}, results) }, results)
end) end)
end) end)
@ -123,15 +132,18 @@ describe("frecency", function()
describe("when opening again", function() describe("when opening again", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch11 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T01:00:00+09:00") local epoch2 = make_epoch "2023-07-29T01:00:00+09:00"
register("hoge1.txt", "2023-07-29T02:00:00+09:00", true) 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() 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({ assert.are.same({
{ count = 2, path = filepath(dir, "hoge1.txt"), score = 40 }, { count = 2, path = filepath(dir, "hoge1.txt"), score = 40, timestamps = { epoch11, epoch12 } },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
}, results) }, results)
end) end)
end) end)
@ -140,15 +152,18 @@ describe("frecency", function()
describe("when opening again but the same instance", function() describe("when opening again but the same instance", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch11 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T01:00:00+09:00") local epoch2 = make_epoch "2023-07-29T01:00:00+09:00"
register("hoge1.txt", "2023-07-29T02: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() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch11 } },
}, results) }, results)
end) end)
end) end)
@ -157,27 +172,62 @@ describe("frecency", function()
describe("when opening more than 10 times", function() describe("when opening more than 10 times", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch11 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge1.txt", "2023-07-29T00:01:00+09:00", true) 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") local epoch201 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01:00+09:00", true) local epoch202 = make_epoch "2023-07-29T00:01:00+09:00"
register("hoge2.txt", "2023-07-29T00:02:00+09:00", true) local epoch203 = make_epoch "2023-07-29T00:02:00+09:00"
register("hoge2.txt", "2023-07-29T00:03:00+09:00", true) local epoch204 = make_epoch "2023-07-29T00:03:00+09:00"
register("hoge2.txt", "2023-07-29T00:04:00+09:00", true) local epoch205 = make_epoch "2023-07-29T00:04:00+09:00"
register("hoge2.txt", "2023-07-29T00:05:00+09:00", true) local epoch206 = make_epoch "2023-07-29T00:05:00+09:00"
register("hoge2.txt", "2023-07-29T00:06:00+09:00", true) local epoch207 = make_epoch "2023-07-29T00:06:00+09:00"
register("hoge2.txt", "2023-07-29T00:07:00+09:00", true) local epoch208 = make_epoch "2023-07-29T00:07:00+09:00"
register("hoge2.txt", "2023-07-29T00:08:00+09:00", true) local epoch209 = make_epoch "2023-07-29T00:08:00+09:00"
register("hoge2.txt", "2023-07-29T00:09:00+09:00", true) local epoch210 = make_epoch "2023-07-29T00:09:00+09:00"
register("hoge2.txt", "2023-07-29T00:10:00+09:00", true) local epoch211 = make_epoch "2023-07-29T00:10:00+09:00"
register("hoge2.txt", "2023-07-29T00:11:00+09:00", true) 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() 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({ 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) }, results)
end) end)
end) end)
@ -202,11 +252,14 @@ describe("frecency", function()
table.insert(expected, { count = 1, path = filepath(dir, file), score = 10 }) table.insert(expected, { count = 1, path = filepath(dir, file), score = 10 })
-- HACK: disable log because it fails with too many logging -- HACK: disable log because it fails with too many logging
log.new({ level = "info" }, true) 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) log.new({}, true)
end end
local start = os.clock() 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) table.sort(results, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
@ -229,14 +282,16 @@ describe("frecency", function()
describe("when no files are unlinked", function() describe("when no files are unlinked", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01: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() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } },
}, results) }, results)
end) end)
end) end)
@ -249,26 +304,31 @@ describe("frecency", function()
{ db_validate_threshold = 3 }, { db_validate_threshold = 3 },
function(frecency, finder, dir) function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01:00+09:00") local epoch2 = make_epoch "2023-07-29T00:01:00+09:00"
register("hoge3.txt", "2023-07-29T00:02:00+09:00") local epoch3 = make_epoch "2023-07-29T00:02:00+09:00"
register("hoge4.txt", "2023-07-29T00:03:00+09:00") local epoch4 = make_epoch "2023-07-29T00:03:00+09:00"
register("hoge5.txt", "2023-07-29T00:04: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("hoge1.txt"):rm()
dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge2.txt"):rm()
frecency:validate_database() frecency:validate_database()
it("removes no entries", function() 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) table.sort(results, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
assert.are.same({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
{ count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge3.txt"), score = 10, timestamps = { epoch3 } },
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } },
}, results) }, results)
end) end)
end end
@ -282,11 +342,16 @@ describe("frecency", function()
{ db_validate_threshold = 3 }, { db_validate_threshold = 3 },
function(frecency, finder, dir) function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01:00+09:00") local epoch2 = make_epoch "2023-07-29T00:01:00+09:00"
register("hoge3.txt", "2023-07-29T00:02:00+09:00") local epoch3 = make_epoch "2023-07-29T00:02:00+09:00"
register("hoge4.txt", "2023-07-29T00:03:00+09:00") local epoch4 = make_epoch "2023-07-29T00:03:00+09:00"
register("hoge5.txt", "2023-07-29T00:04: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("hoge1.txt"):rm()
dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge2.txt"):rm()
dir:joinpath("hoge3.txt"):rm() dir:joinpath("hoge3.txt"):rm()
@ -300,13 +365,13 @@ describe("frecency", function()
end) end)
it("removes entries", function() 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) table.sort(results, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
assert.are.same({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } },
}, results) }, results)
end) end)
end end
@ -319,11 +384,16 @@ describe("frecency", function()
{ db_validate_threshold = 3 }, { db_validate_threshold = 3 },
function(frecency, finder, dir) function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01:00+09:00") local epoch2 = make_epoch "2023-07-29T00:01:00+09:00"
register("hoge3.txt", "2023-07-29T00:02:00+09:00") local epoch3 = make_epoch "2023-07-29T00:02:00+09:00"
register("hoge4.txt", "2023-07-29T00:03:00+09:00") local epoch4 = make_epoch "2023-07-29T00:03:00+09:00"
register("hoge5.txt", "2023-07-29T00:04: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("hoge1.txt"):rm()
dir:joinpath("hoge2.txt"):rm() dir:joinpath("hoge2.txt"):rm()
dir:joinpath("hoge3.txt"):rm() dir:joinpath("hoge3.txt"):rm()
@ -337,16 +407,16 @@ describe("frecency", function()
end) end)
it("removes no entries", function() 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) table.sort(results, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
assert.are.same({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
{ count = 1, path = filepath(dir, "hoge3.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge3.txt"), score = 10, timestamps = { epoch3 } },
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge4.txt"), score = 10, timestamps = { epoch4 } },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } },
}, results) }, results)
end) end)
end end
@ -359,8 +429,10 @@ describe("frecency", function()
describe("when db_safe_mode is true", function() describe("when db_safe_mode is true", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01: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() dir:joinpath("hoge1.txt"):rm()
with_fake_vim_ui_select("y", function(called) with_fake_vim_ui_select("y", function(called)
@ -372,9 +444,9 @@ describe("frecency", function()
end) end)
it("needs confirmation for removing entries", function() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
}, results) }, results)
end) end)
end) end)
@ -383,8 +455,10 @@ describe("frecency", function()
describe("when db_safe_mode is false", function() describe("when db_safe_mode is false", function()
with_files({ "hoge1.txt", "hoge2.txt" }, { db_safe_mode = false }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, { db_safe_mode = false }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01: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() dir:joinpath("hoge1.txt"):rm()
with_fake_vim_ui_select("y", function(called) with_fake_vim_ui_select("y", function(called)
@ -396,9 +470,9 @@ describe("frecency", function()
end) end)
it("needs no confirmation for removing entries", function() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge2.txt"), score = 10, timestamps = { epoch2 } },
}, results) }, results)
end) end)
end) end)
@ -410,8 +484,10 @@ describe("frecency", function()
describe("when file exists", function() describe("when file exists", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir) with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00") local epoch1 = make_epoch "2023-07-29T00:00:00+09:00"
register("hoge2.txt", "2023-07-29T00:01: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() it("deletes the file successfully", function()
local path = filepath(dir, "hoge2.txt") local path = filepath(dir, "hoge2.txt")
@ -426,12 +502,97 @@ describe("frecency", function()
end) end)
it("returns valid results", function() 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({ assert.are.same({
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 }, { count = 1, path = filepath(dir, "hoge1.txt"), score = 10, timestamps = { epoch1 } },
}, results) }, results)
end) end)
end) end)
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) end)

View File

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

View File

@ -4,6 +4,7 @@
---@class FrecencyInstance ---@class FrecencyInstance
---@field complete fun(findstart: 1|0, base: string): integer|''|string[] ---@field complete fun(findstart: 1|0, base: string): integer|''|string[]
---@field delete fun(path: string): nil ---@field delete fun(path: string): nil
---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[]
---@field register fun(bufnr: integer, datetime: string?): nil ---@field register fun(bufnr: integer, datetime: string?): nil
---@field start fun(opts: FrecencyPickerOptions?): nil ---@field start fun(opts: FrecencyPickerOptions?): nil
---@field validate_database fun(force: boolean?): nil ---@field validate_database fun(force: boolean?): nil
@ -31,6 +32,7 @@ return require("telescope").register_extension {
exports = { exports = {
frecency = frecency.start, frecency = frecency.start,
complete = frecency.complete, complete = frecency.complete,
query = frecency.query,
}, },
---When this func is called, Frecency instance is NOT created but only ---When this func is called, Frecency instance is NOT created but only