fix!: register realpath for consistency (#240)

Now it uses realpath for registering and validating DB. This means, if you have entries that has filenames differing only for case, it can deal with them as they exist.

Before this, it has miscalculated scores for such cases. For example, in case you have `/path/to/foo.lua` and `/path/to/Foo.lua`, it registers entries for each file. Now it detects accurate filename for the specified one, and removes it in validation.

* test: separate logic for utils

* fix!: register realpath for consistency

* refactor: convert fs module from class

* refactor: move db initialization phase to start()

* fix: run database:start() truly asynchronously

* fix: call each functions with async wrapping

* refactor: add types for args in command

* fix: run register() synchronously

Because vim.api.nvim_* cannot be used in asynchronous functions.

* docs: add note for calling setup() twice

* fix: run non-fast logic on next tick
This commit is contained in:
JINNOUCHI Yasushi 2024-08-15 17:40:03 +09:00 committed by GitHub
parent 39f70a87a2
commit 58c0089414
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 710 additions and 499 deletions

View File

@ -326,14 +326,15 @@ This will be called by |telescope.nvim| for its initialization. You can also
call this to initialize this plugin separated from |telescope.nvim|'s call this to initialize this plugin separated from |telescope.nvim|'s
initialization phase. initialization phase.
This is useful when you want to load |telescope.nvim| lazily, but want to This is useful when you want to load |telescope.nvim| lazily and want to
register opened files as soon as Neovim has started. Example configuration for register opened files as soon as Neovim has started. Example configuration for
|lazy.nvim| is below. |lazy.nvim| is below.
>lua >lua
{ {
"nvim-telescope/telescope-frecency.nvim", "nvim-telescope/telescope-frecency.nvim",
main = "frecency", main = "frecency",
---@type FrecencyOpts -- `opts` property calls the plugin's setup() function.
-- In this case, this calls frecency.setup().
opts = { opts = {
db_safe_mode = false, db_safe_mode = false,
}, },
@ -358,7 +359,10 @@ register opened files as soon as Neovim has started. Example configuration for
telescope.load_extension "frecency" telescope.load_extension "frecency"
end, end,
}, },
<
This function does nothing when it is called for the second times and later.
If you want to set another configuration, use
|telescope-frecency-configuration-config.setup()|.
============================================================================== ==============================================================================
CONFIGURATION *telescope-frecency-configuration* CONFIGURATION *telescope-frecency-configuration*

View File

@ -26,6 +26,7 @@ local os_util = require "frecency.os_util"
---@class FrecencyConfig: FrecencyRawConfig ---@class FrecencyConfig: FrecencyRawConfig
---@field ext_config FrecencyRawConfig ---@field ext_config FrecencyRawConfig
---@field private cached_ignore_regexes? string[]
---@field private values FrecencyRawConfig ---@field private values FrecencyRawConfig
local Config = {} local Config = {}
@ -79,6 +80,7 @@ Config.new = function()
workspaces = true, workspaces = true,
} }
return setmetatable({ return setmetatable({
cached_ignore_regexes = {},
ext_config = {}, ext_config = {},
values = Config.default_values, values = Config.default_values,
}, { }, {
@ -138,6 +140,17 @@ Config.get = function()
return config.values return config.values
end end
---@return string[]
Config.ignore_regexes = function()
if not config.cached_ignore_regexes then
config.cached_ignore_regexes = vim.tbl_map(function(pattern)
local regex = vim.pesc(pattern):gsub("%%%*", ".*"):gsub("%%%?", ".")
return "^" .. regex .. "$"
end, config.ignore_patterns)
end
return config.cached_ignore_regexes
end
---@param ext_config any ---@param ext_config any
---@return nil ---@return nil
Config.setup = function(ext_config) Config.setup = function(ext_config)
@ -174,6 +187,7 @@ Config.setup = function(ext_config)
workspace_scan_cmd = { opts.workspace_scan_cmd, { "s", "t" }, true }, workspace_scan_cmd = { opts.workspace_scan_cmd, { "s", "t" }, true },
workspaces = { opts.workspaces, "t" }, workspaces = { opts.workspaces, "t" },
} }
config.cached_ignore_regexes = nil
config.ext_config = ext_config config.ext_config = ext_config
config.values = opts config.values = opts
end end

View File

@ -1,6 +1,7 @@
local Table = require "frecency.database.table" local Table = require "frecency.database.table"
local FileLock = require "frecency.file_lock" local FileLock = require "frecency.file_lock"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs"
local watcher = require "frecency.watcher" local watcher = require "frecency.watcher"
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
@ -13,48 +14,73 @@ local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
---@field score number ---@field score number
---@field timestamps integer[] ---@field timestamps integer[]
---@alias FrecencyDatabaseVersion "v1"
---@class FrecencyDatabase ---@class FrecencyDatabase
---@field tx FrecencyPlenaryAsyncControlChannelTx ---@field private _file_lock FrecencyFileLock
---@field private file_lock FrecencyFileLock ---@field private file_lock_rx async fun(): ...
---@field private filename string ---@field private file_lock_tx fun(...): nil
---@field private fs FrecencyFS
---@field private tbl FrecencyDatabaseTable ---@field private tbl FrecencyDatabaseTable
---@field private version "v1" ---@field private version FrecencyDatabaseVersion
---@field private watcher_rx FrecencyPlenaryAsyncControlChannelRx
---@field private watcher_tx FrecencyPlenaryAsyncControlChannelTx
local Database = {} local Database = {}
---@param fs FrecencyFS
---@return FrecencyDatabase ---@return FrecencyDatabase
Database.new = function(fs) Database.new = function()
local version = "v1" local version = "v1"
local self = setmetatable({ local file_lock_tx, file_lock_rx = async.control.channel.oneshot()
fs = fs, local watcher_tx, watcher_rx = async.control.channel.mpsc()
return setmetatable({
file_lock_rx = file_lock_rx,
file_lock_tx = file_lock_tx,
tbl = Table.new(version), tbl = Table.new(version),
version = version, version = version,
watcher_rx = watcher_rx,
watcher_tx = watcher_tx,
}, { __index = Database }) }, { __index = Database })
self.filename = (function() end
---@async
---@return string
function Database:filename()
local file_v1 = "file_frecency.bin"
---@async
---@return string
local function filename_v1()
-- NOTE: for backward compatibility -- NOTE: for backward compatibility
-- If the user does not set db_root specifically, search DB in -- If the user does not set db_root specifically, search DB in
-- $XDG_DATA_HOME/nvim in addition to $XDG_STATE_HOME/nvim (default value). -- $XDG_DATA_HOME/nvim in addition to $XDG_STATE_HOME/nvim (default value).
local file = "file_frecency.bin" local db = Path.new(config.db_root, file_v1).filename
local db = Path.new(config.db_root, file) if not config.ext_config.db_root and not fs.exists(db) then
if not config.ext_config.db_root and not db:exists() then local old_location = Path.new(vim.fn.stdpath "data", file_v1).filename
local old_location = Path.new(vim.fn.stdpath "data", file) if fs.exists(old_location) then
if old_location:exists() then return old_location
return old_location.filename
end end
end end
return db.filename return db
end)() end
self.file_lock = FileLock.new(self.filename)
local rx if self.version == "v1" then
self.tx, rx = async.control.channel.mpsc() return filename_v1()
self.tx.send "load" else
watcher.watch(self.filename, function() error(("unknown version: %s"):format(self.version))
self.tx.send "load" end
end
---@async
---@return nil
function Database:start()
local target = self:filename()
self.file_lock_tx(FileLock.new(target))
self.watcher_tx.send "load"
watcher.watch(target, function()
self.watcher_tx.send "load"
end) end)
async.void(function() async.void(function()
while true do while true do
local mode = rx.recv() local mode = self.watcher_rx.recv()
log.debug("DB coroutine start:", mode) log.debug("DB coroutine start:", mode)
if mode == "load" then if mode == "load" then
self:load() self:load()
@ -66,14 +92,15 @@ Database.new = function(fs)
log.debug("DB coroutine end:", mode) log.debug("DB coroutine end:", mode)
end end
end)() end)()
return self
end end
---@async
---@return boolean ---@return boolean
function Database:has_entry() function Database:has_entry()
return not vim.tbl_isempty(self.tbl.records) return not vim.tbl_isempty(self.tbl.records)
end end
---@async
---@param paths string[] ---@param paths string[]
---@return nil ---@return nil
function Database:insert_files(paths) function Database:insert_files(paths)
@ -83,28 +110,32 @@ function Database:insert_files(paths)
for _, path in ipairs(paths) do for _, path in ipairs(paths) do
self.tbl.records[path] = { count = 1, timestamps = { 0 } } self.tbl.records[path] = { count = 1, timestamps = { 0 } }
end end
self.tx.send "save" self.watcher_tx.send "save"
end end
---@async
---@return string[] ---@return string[]
function Database:unlinked_entries() function Database:unlinked_entries()
local paths = {} return vim.tbl_flatten(async.util.join(vim.tbl_map(function(path)
for file in pairs(self.tbl.records) do return function()
if not self.fs:is_valid_path(file) then local err, realpath = async.uv.fs_realpath(path)
table.insert(paths, file) if err or not realpath or realpath ~= path or fs.is_ignored(realpath) then
return path
end end
end end
return paths end, vim.tbl_keys(self.tbl.records))))
end end
---@async
---@param paths string[] ---@param paths string[]
function Database:remove_files(paths) function Database:remove_files(paths)
for _, file in ipairs(paths) do for _, file in ipairs(paths) do
self.tbl.records[file] = nil self.tbl.records[file] = nil
end end
self.tx.send "save" self.watcher_tx.send "save"
end end
---@async
---@param path string ---@param path string
---@param epoch? integer ---@param epoch? integer
function Database:update(path, epoch) function Database:update(path, epoch)
@ -120,9 +151,10 @@ function Database:update(path, epoch)
record.timestamps = new_table record.timestamps = new_table
end end
self.tbl.records[path] = record self.tbl.records[path] = record
self.tx.send "save" self.watcher_tx.send "save"
end end
---@async
---@param workspace? string ---@param workspace? string
---@param epoch? integer ---@param epoch? integer
---@return FrecencyDatabaseEntry[] ---@return FrecencyDatabaseEntry[]
@ -130,7 +162,7 @@ function Database:get_entries(workspace, epoch)
local now = epoch or os.time() 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 fs.starts_with(path, workspace) then
table.insert(items, { table.insert(items, {
path = path, path = path,
count = record.count, count = record.count,
@ -148,13 +180,13 @@ end
---@return nil ---@return nil
function Database:load() function Database:load()
local start = os.clock() local start = os.clock()
local err, data = self.file_lock:with(function() local err, data = self:file_lock():with(function(target)
local err, stat = async.uv.fs_stat(self.filename) local err, stat = async.uv.fs_stat(target)
if err then if err then
return nil return nil
end end
local fd local fd
err, fd = async.uv.fs_open(self.filename, "r", tonumber("644", 8)) err, fd = async.uv.fs_open(target, "r", tonumber("644", 8))
assert(not err, err) assert(not err, err)
local data local data
err, data = async.uv.fs_read(fd, stat.size) err, data = async.uv.fs_read(fd, stat.size)
@ -173,9 +205,9 @@ end
---@return nil ---@return nil
function Database:save() function Database:save()
local start = os.clock() local start = os.clock()
local err = self.file_lock:with(function() local err = self:file_lock():with(function(target)
self:raw_save(self.tbl:raw()) self:raw_save(self.tbl:raw(), target)
local err, stat = async.uv.fs_stat(self.filename) local err, stat = async.uv.fs_stat(target)
assert(not err, err) assert(not err, err)
watcher.update(stat) watcher.update(stat)
return nil return nil
@ -185,16 +217,18 @@ function Database:save()
end end
---@async ---@async
---@param target string
---@param tbl FrecencyDatabaseRawTable ---@param tbl FrecencyDatabaseRawTable
function Database:raw_save(tbl) function Database:raw_save(tbl, target)
local f = assert(load("return " .. vim.inspect(tbl))) local f = assert(load("return " .. vim.inspect(tbl)))
local data = string.dump(f) local data = string.dump(f)
local err, fd = async.uv.fs_open(self.filename, "w", tonumber("644", 8)) local err, fd = async.uv.fs_open(target, "w", tonumber("644", 8))
assert(not err, err) assert(not err, err)
assert(not async.uv.fs_write(fd, data)) assert(not async.uv.fs_write(fd, data))
assert(not async.uv.fs_close(fd)) assert(not async.uv.fs_close(fd))
end end
---@async
---@param path string ---@param path string
---@return boolean ---@return boolean
function Database:remove_entry(path) function Database:remove_entry(path)
@ -202,8 +236,18 @@ function Database:remove_entry(path)
return false return false
end end
self.tbl.records[path] = nil self.tbl.records[path] = nil
self.tx.send "save" self.watcher_tx.send "save"
return true return true
end end
---@private
---@async
---@return FrecencyFileLock
function Database:file_lock()
if not self._file_lock then
self._file_lock = self.file_lock_rx()
end
return self._file_lock
end
return Database return Database

View File

@ -1,4 +1,5 @@
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async"
---@class FrecencyDatabaseRecordValue ---@class FrecencyDatabaseRecordValue
---@field count integer ---@field count integer
@ -18,14 +19,12 @@ Table.new = function(version)
return setmetatable({ is_ready = false, version = version }, { __index = Table.__index }) return setmetatable({ is_ready = false, version = version }, { __index = Table.__index })
end end
---@async
---@param key string
function Table:__index(key) function Table:__index(key)
if key == "records" and not rawget(self, "is_ready") then if key == "records" and not rawget(self, "is_ready") then
local start = os.clock()
log.debug "waiting start"
Table.wait_ready(self) Table.wait_ready(self)
log.debug(("waiting until DB become clean takes %f seconds"):format(os.clock() - start))
end end
log.debug(("is_ready: %s, key: %s, value: %s"):format(rawget(self, "is_ready"), key, rawget(self, key)))
return vim.F.if_nil(rawget(self, key), Table[key]) return vim.F.if_nil(rawget(self, key), Table[key])
end end
@ -45,11 +44,16 @@ function Table:set(raw_table)
end end
---This is for internal or testing use only. ---This is for internal or testing use only.
---@async
---@return nil ---@return nil
function Table:wait_ready() function Table:wait_ready()
vim.wait(2000, function() local start = os.clock()
return rawget(self, "is_ready") local t = 0.2
end) while not rawget(self, "is_ready") do
async.util.sleep(t)
t = t * 2
end
log.debug(("wait_ready() takes %f seconds"):format(os.clock() - start))
end end
return Table return Table

View File

@ -1,19 +1,18 @@
local WebDevicons = require "frecency.web_devicons" local WebDevicons = require "frecency.web_devicons"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
local entry_display = require "telescope.pickers.entry_display" --[[@as FrecencyTelescopeEntryDisplay]] local entry_display = require "telescope.pickers.entry_display" --[[@as FrecencyTelescopeEntryDisplay]]
local utils = require "telescope.utils" --[[@as FrecencyTelescopeUtils]] local utils = require "telescope.utils" --[[@as FrecencyTelescopeUtils]]
---@class FrecencyEntryMaker ---@class FrecencyEntryMaker
---@field fs FrecencyFS
---@field loaded table<string,boolean> ---@field loaded table<string,boolean>
---@field web_devicons WebDevicons ---@field web_devicons WebDevicons
local EntryMaker = {} local EntryMaker = {}
---@param fs FrecencyFS
---@return FrecencyEntryMaker ---@return FrecencyEntryMaker
EntryMaker.new = function(fs) EntryMaker.new = function()
return setmetatable({ fs = fs, web_devicons = WebDevicons.new() }, { __index = EntryMaker }) return setmetatable({ web_devicons = WebDevicons.new() }, { __index = EntryMaker })
end end
---@class FrecencyEntry ---@class FrecencyEntry
@ -128,7 +127,7 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
end end
if config.show_filter_column and workspace and workspace_tag then if config.show_filter_column and workspace and workspace_tag then
local filtered = self:should_show_tail(workspace_tag) and utils.path_tail(workspace) .. Path.path.sep local filtered = self:should_show_tail(workspace_tag) and utils.path_tail(workspace) .. Path.path.sep
or self.fs:relative_from_home(workspace) .. Path.path.sep or fs.relative_from_home(workspace) .. Path.path.sep
table.insert(items, { filtered, "Directory" }) table.insert(items, { filtered, "Directory" })
end end
local formatted_name, path_style = formatter(entry.name) local formatted_name, path_style = formatter(entry.name)
@ -153,7 +152,7 @@ end
---@return integer ---@return integer
function EntryMaker:calculate_filter_column_width(workspace, workspace_tag) function EntryMaker:calculate_filter_column_width(workspace, workspace_tag)
return self:should_show_tail(workspace_tag) and #(utils.path_tail(workspace)) + 1 return self:should_show_tail(workspace_tag) and #(utils.path_tail(workspace)) + 1
or #(self.fs:relative_from_home(workspace)) + 1 or #(fs.relative_from_home(workspace)) + 1
end end
---@private ---@private

View File

@ -4,23 +4,28 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class FrecencyFileLock ---@class FrecencyFileLock
---@field base string ---@field base string
---@field config FrecencyFileLockConfig ---@field config FrecencyFileLockRawConfig
---@field filename string ---@field lock string
---@field target string
local FileLock = {} local FileLock = {}
---@class FrecencyFileLockConfig ---@class FrecencyFileLockConfig
---@field retry? integer default: 5
---@field unlink_retry? integer default: 5
---@field interval? integer default: 500
---@class FrecencyFileLockRawConfig
---@field retry integer default: 5 ---@field retry integer default: 5
---@field unlink_retry integer default: 5 ---@field unlink_retry integer default: 5
---@field interval integer default: 500 ---@field interval integer default: 500
---@param path string ---@param target string
---@param file_lock_config? FrecencyFileLockConfig ---@param file_lock_config? FrecencyFileLockConfig
---@return FrecencyFileLock ---@return FrecencyFileLock
FileLock.new = function(path, file_lock_config) FileLock.new = function(target, file_lock_config)
log.debug(("file_lock new(): %s"):format(target))
local config = vim.tbl_extend("force", { retry = 5, unlink_retry = 5, interval = 500 }, file_lock_config or {}) local config = vim.tbl_extend("force", { retry = 5, unlink_retry = 5, interval = 500 }, file_lock_config or {})
local self = setmetatable({ config = config }, { __index = FileLock }) return setmetatable({ config = config, lock = target .. ".lock", target = target }, { __index = FileLock })
self.filename = path .. ".lock"
return self
end end
---@async ---@async
@ -31,21 +36,21 @@ function FileLock:get()
local err, fd local err, fd
while true do while true do
count = count + 1 count = count + 1
local dir = Path.new(self.filename):parent() local dir = Path.new(self.lock):parent()
if not dir:exists() then if not dir:exists() then
-- TODO: make this call be async -- TODO: make this call be async
log.debug(("file_lock get(): mkdir parent: %s"):format(dir.filename)) log.debug(("file_lock get(): mkdir parent: %s"):format(dir.filename))
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
dir:mkdir { parents = true } dir:mkdir { parents = true }
end end
err, fd = async.uv.fs_open(self.filename, "wx", tonumber("600", 8)) err, fd = async.uv.fs_open(self.lock, "wx", tonumber("600", 8))
if not err then if not err then
break break
end end
async.util.sleep(self.config.interval) async.util.sleep(self.config.interval)
if count >= self.config.retry then if count >= self.config.retry then
log.debug(("file_lock get(): retry count reached. try to delete the lock file: %d"):format(count)) log.debug(("file_lock get(): retry count reached. try to delete the lock file: %d"):format(count))
err = async.uv.fs_unlink(self.filename) err = async.uv.fs_unlink(self.lock)
if err then if err then
log.debug("file_lock get() failed: " .. err) log.debug("file_lock get() failed: " .. err)
unlink_count = unlink_count + 1 unlink_count = unlink_count + 1
@ -67,12 +72,12 @@ end
---@async ---@async
---@return string? err ---@return string? err
function FileLock:release() function FileLock:release()
local err = async.uv.fs_stat(self.filename) local err = async.uv.fs_stat(self.lock)
if err then if err then
log.debug("file_lock release() not found: " .. err) log.debug("file_lock release() not found: " .. err)
return "lock not found" return "lock not found"
end end
err = async.uv.fs_unlink(self.filename) err = async.uv.fs_unlink(self.lock)
if err then if err then
log.debug("file_lock release() unlink failed: " .. err) log.debug("file_lock release() unlink failed: " .. err)
return err return err
@ -81,7 +86,7 @@ end
---@async ---@async
---@generic T ---@generic T
---@param f fun(): T ---@param f fun(target: string): T
---@return string? err ---@return string? err
---@return T ---@return T
function FileLock:with(f) function FileLock:with(f)
@ -89,7 +94,7 @@ function FileLock:with(f)
if err then if err then
return err, nil return err, nil
end end
local ok, result_or_err = pcall(f) local ok, result_or_err = pcall(f, self.target)
err = self:release() err = self:release()
if err then if err then
return err, nil return err, nil

View File

@ -1,4 +1,5 @@
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs"
local os_util = require "frecency.os_util" local os_util = require "frecency.os_util"
local log = require "frecency.log" local log = require "frecency.log"
local Job = require "plenary.job" local Job = require "plenary.job"
@ -10,7 +11,6 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@field entries FrecencyEntry[] ---@field entries FrecencyEntry[]
---@field scanned_entries FrecencyEntry[] ---@field scanned_entries FrecencyEntry[]
---@field entry_maker FrecencyEntryMakerInstance ---@field entry_maker FrecencyEntryMakerInstance
---@field fs FrecencyFS
---@field path? string ---@field path? string
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private rx FrecencyPlenaryAsyncControlChannelRx ---@field private rx FrecencyPlenaryAsyncControlChannelRx
@ -32,14 +32,13 @@ local Finder = {}
---@param database FrecencyDatabase ---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMakerInstance ---@param entry_maker FrecencyEntryMakerInstance
---@param fs FrecencyFS
---@param need_scandir boolean ---@param need_scandir boolean
---@param path string? ---@param path string?
---@param recency FrecencyRecency ---@param recency FrecencyRecency
---@param state FrecencyState ---@param state FrecencyState
---@param finder_config? FrecencyFinderConfig ---@param finder_config? FrecencyFinderConfig
---@return FrecencyFinder ---@return FrecencyFinder
Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, state, finder_config) Finder.new = function(database, entry_maker, need_scandir, path, recency, state, finder_config)
local tx, rx = async.control.channel.mpsc() local tx, rx = async.control.channel.mpsc()
local scan_tx, scan_rx = async.control.channel.mpsc() local scan_tx, scan_rx = async.control.channel.mpsc()
local self = setmetatable({ local self = setmetatable({
@ -47,7 +46,6 @@ Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, st
closed = false, closed = false,
database = database, database = database,
entry_maker = entry_maker, entry_maker = entry_maker,
fs = fs,
path = path, path = path,
recency = recency, recency = recency,
state = state, state = state,
@ -168,7 +166,7 @@ end
---@return nil ---@return nil
function Finder:scan_dir_lua() function Finder:scan_dir_lua()
local count = 0 local count = 0
for name in self.fs:scan_dir(self.path) do for name in fs.scan_dir(self.path) do
if self.closed then if self.closed then
break break
end end
@ -296,6 +294,8 @@ function Finder:reflow_results()
return return
end end
async.util.scheduler() async.util.scheduler()
local function reflow()
local bufnr = picker.results_bufnr local bufnr = picker.results_bufnr
local win = picker.results_win local win = picker.results_win
if not bufnr or not win or not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_win_is_valid(win) then if not bufnr or not win or not vim.api.nvim_buf_is_valid(bufnr) or not vim.api.nvim_win_is_valid(win) then
@ -317,4 +317,11 @@ function Finder:reflow_results()
end end
end end
if vim.in_fast_event() then
reflow()
else
vim.schedule(reflow)
end
end
return Finder return Finder

View File

@ -2,57 +2,57 @@ local config = require "frecency.config"
local os_util = require "frecency.os_util" local os_util = require "frecency.os_util"
local log = require "frecency.log" local log = require "frecency.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local scandir = require "plenary.scandir" local scandir = require "plenary.scandir"
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
---@class FrecencyFS local M = {
---@field os_homedir string os_homedir = assert(uv.os_homedir()),
---@field private config FrecencyFSConfig }
---@field private ignore_regexes string[]
local FS = {}
---@class FrecencyFSConfig -- TODO: make this configurable
---@field scan_depth integer? local SCAN_DEPTH = 100
---@param fs_config? FrecencyFSConfig ---@param path string
---@return FrecencyFS ---@return boolean
FS.new = function(fs_config) function M.is_ignored(path)
local self= setmetatable( for _, regex in ipairs(config.ignore_regexes()) do
{ config = vim.tbl_extend("force", { scan_depth = 100 }, fs_config or {}), os_homedir = assert(uv.os_homedir()) }, if path:find(regex) then
{ __index = FS } return true
) end
---@param pattern string end
self.ignore_regexes = vim.tbl_map(function(pattern) return false
local regex = vim.pesc(pattern):gsub("%%%*", ".*"):gsub("%%%?", ".")
return "^" .. regex .. "$"
end, config.ignore_patterns)
return self
end end
---@async
---@param path? string ---@param path? string
---@return boolean ---@return boolean
function FS:is_valid_path(path) function M.is_valid_path(path)
return not not path and Path:new(path):is_file() and not self:is_ignored(path) if not path then
return false
end
local err, st = async.uv.fs_stat(path)
return not err and st.type == "file" and not M.is_ignored(path)
end end
---@param path string ---@param path string
---@return function ---@return function
function FS:scan_dir(path) function M.scan_dir(path)
log.debug { path = path } log.debug { path = path }
local gitignore = self:make_gitignore(path) local gitignore = M.make_gitignore(path)
return coroutine.wrap(function() return coroutine.wrap(function()
for name, type in for name, type in
vim.fs.dir(path, { vim.fs.dir(path, {
depth = self.config.scan_depth, depth = SCAN_DEPTH,
skip = function(dirname) skip = function(dirname)
if self:is_ignored(os_util.join_path(path, dirname)) then if M.is_ignored(os_util.join_path(path, dirname)) then
return false return false
end end
end, end,
}) })
do do
local fullpath = os_util.join_path(path, name) local fullpath = os_util.join_path(path, name)
if type == "file" and not self:is_ignored(fullpath) and gitignore({ path }, fullpath) then if type == "file" and not M.is_ignored(fullpath) and gitignore({ path }, fullpath) then
coroutine.yield(name) coroutine.yield(name)
end end
end end
@ -61,8 +61,8 @@ end
---@param path string ---@param path string
---@return string ---@return string
function FS:relative_from_home(path) function M.relative_from_home(path)
return Path:new(path):make_relative(self.os_homedir) return Path:new(path):make_relative(M.os_homedir)
end end
---@type table<string,string> ---@type table<string,string>
@ -71,7 +71,7 @@ local with_sep = {}
---@param path string ---@param path string
---@param base? string ---@param base? string
---@return boolean ---@return boolean
function FS:starts_with(path, base) function M.starts_with(path, base)
if not base then if not base then
return true return true
end end
@ -81,25 +81,20 @@ function FS:starts_with(path, base)
return path:find(with_sep[base], 1, true) == 1 return path:find(with_sep[base], 1, true) == 1
end end
---@private ---@async
---@param path string ---@param path string
---@return boolean ---@return boolean
function FS:is_ignored(path) function M.exists(path)
for _, regex in ipairs(self.ignore_regexes) do return not (async.uv.fs_stat(path))
if path:find(regex) then
return true
end
end
return false
end end
---@private ---@private
---@param basepath string ---@param basepath string
---@return fun(base_paths: string[], entry: string): boolean ---@return fun(base_paths: string[], entry: string): boolean
function FS:make_gitignore(basepath) function M.make_gitignore(basepath)
return scandir.__make_gitignore { basepath } or function(_, _) return scandir.__make_gitignore { basepath } or function(_, _)
return true return true
end end
end end
return FS return M

View File

@ -3,11 +3,11 @@
---setup() to be initialized. ---setup() to be initialized.
---@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 async fun(path: string): nil
---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[] ---@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 async fun(force: boolean?): nil
local frecency = setmetatable({}, { local frecency = setmetatable({}, {
---@param self FrecencyInstance ---@param self FrecencyInstance
---@param key "complete"|"delete"|"register"|"start"|"validate_database" ---@param key "complete"|"delete"|"register"|"start"|"validate_database"
@ -28,6 +28,10 @@ local frecency = setmetatable({}, {
end, end,
}) })
local function async_call(f, ...)
require("plenary.async").void(f)(...)
end
local setup_done = false local setup_done = false
---When this func is called, Frecency instance is NOT created but only ---When this func is called, Frecency instance is NOT created but only
@ -46,15 +50,20 @@ local function setup(ext_config)
vim.api.nvim_set_hl(0, "TelescopeFrecencyScores", { link = "Number", default = true }) vim.api.nvim_set_hl(0, "TelescopeFrecencyScores", { link = "Number", default = true })
vim.api.nvim_set_hl(0, "TelescopeQueryFilter", { link = "WildMenu", default = true }) vim.api.nvim_set_hl(0, "TelescopeQueryFilter", { link = "WildMenu", default = true })
---@param cmd_info { bang: boolean } ---@class FrecencyCommandInfo
---@field args string
---@field bang boolean
---@param cmd_info FrecencyCommandInfo
vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info) vim.api.nvim_create_user_command("FrecencyValidate", function(cmd_info)
frecency.validate_database(cmd_info.bang) async_call(frecency.validate_database, cmd_info.bang)
end, { bang = true, desc = "Clean up DB for telescope-frecency" }) end, { bang = true, desc = "Clean up DB for telescope-frecency" })
vim.api.nvim_create_user_command("FrecencyDelete", function(info) ---@param cmd_info FrecencyCommandInfo
local path_string = info.args == "" and "%:p" or info.args vim.api.nvim_create_user_command("FrecencyDelete", function(cmd_info)
local path_string = cmd_info.args == "" and "%:p" or cmd_info.args
local path = vim.fn.expand(path_string) --[[@as string]] local path = vim.fn.expand(path_string) --[[@as string]]
frecency.delete(path) async_call(frecency.delete, path)
end, { nargs = "?", complete = "file", desc = "Delete entry from telescope-frecency" }) end, { nargs = "?", complete = "file", desc = "Delete entry from telescope-frecency" })
local group = vim.api.nvim_create_augroup("TelescopeFrecency", {}) local group = vim.api.nvim_create_augroup("TelescopeFrecency", {})

View File

@ -1,16 +1,16 @@
local Database = require "frecency.database" local Database = require "frecency.database"
local EntryMaker = require "frecency.entry_maker" local EntryMaker = require "frecency.entry_maker"
local FS = require "frecency.fs"
local Picker = require "frecency.picker" local Picker = require "frecency.picker"
local Recency = require "frecency.recency" local Recency = require "frecency.recency"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs"
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class Frecency ---@class Frecency
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database. ---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private entry_maker FrecencyEntryMaker ---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field private picker FrecencyPicker ---@field private picker FrecencyPicker
---@field private recency FrecencyRecency ---@field private recency FrecencyRecency
local Frecency = {} local Frecency = {}
@ -18,9 +18,8 @@ local Frecency = {}
---@return Frecency ---@return Frecency
Frecency.new = function() Frecency.new = function()
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]] local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]]
self.fs = FS.new() self.database = Database.new()
self.database = Database.new(self.fs) self.entry_maker = EntryMaker.new()
self.entry_maker = EntryMaker.new(self.fs)
self.recency = Recency.new() self.recency = Recency.new()
return self return self
end end
@ -28,14 +27,29 @@ end
---This is called when `:Telescope frecency` is called at the first time. ---This is called when `:Telescope frecency` is called at the first time.
---@return nil ---@return nil
function Frecency:setup() function Frecency:setup()
-- HACK: Wihout this wrapping, it spoils background color detection. local done = false
-- See https://github.com/nvim-telescope/telescope-frecency.nvim/issues/210 ---@async
vim.defer_fn(function() local function init()
self.database:start()
self:assert_db_entries() self:assert_db_entries()
if config.auto_validate then if config.auto_validate then
self:validate_database() self:validate_database()
end end
end, 0) done = true
end
local is_async = not not coroutine.running()
if is_async then
init()
else
async.void(init)()
local ok, status = vim.wait(1000, function()
return done
end)
if not ok then
log.error("failed to setup:", status == -1 and "timed out" or "interrupted")
end
end
end end
---This can be calledBy `require("telescope").extensions.frecency.frecency`. ---This can be calledBy `require("telescope").extensions.frecency.frecency`.
@ -52,7 +66,7 @@ function Frecency:start(opts)
if opts.hide_current_buffer or config.hide_current_buffer then if opts.hide_current_buffer or config.hide_current_buffer then
ignore_filenames = { vim.api.nvim_buf_get_name(0) } ignore_filenames = { vim.api.nvim_buf_get_name(0) }
end end
self.picker = Picker.new(self.database, self.entry_maker, self.fs, self.recency, { self.picker = Picker.new(self.database, self.entry_maker, self.recency, {
editing_bufnr = vim.api.nvim_get_current_buf(), editing_bufnr = vim.api.nvim_get_current_buf(),
ignore_filenames = ignore_filenames, ignore_filenames = ignore_filenames,
initial_workspace_tag = opts.workspace, initial_workspace_tag = opts.workspace,
@ -69,16 +83,7 @@ function Frecency:complete(findstart, base)
return self.picker:complete(findstart, base) return self.picker:complete(findstart, base)
end end
---@private ---@async
---@return nil
function Frecency:assert_db_entries()
if not self.database:has_entry() then
self.database:insert_files(vim.v.oldfiles)
self:notify("Imported %d entries from oldfiles.", #vim.v.oldfiles)
end
end
---@private
---@param force? boolean ---@param force? boolean
---@return nil ---@return nil
function Frecency:validate_database(force) function Frecency:validate_database(force)
@ -110,20 +115,38 @@ function Frecency:validate_database(force)
end) end)
end end
---@private
---@async
---@return nil
function Frecency:assert_db_entries()
if not self.database:has_entry() then
self.database:insert_files(vim.v.oldfiles)
self:notify("Imported %d entries from oldfiles.", #vim.v.oldfiles)
end
end
---@param bufnr integer ---@param bufnr integer
---@param epoch? integer ---@param epoch? integer
function Frecency:register(bufnr, epoch) function Frecency:register(bufnr, epoch)
if config.ignore_register and config.ignore_register(bufnr) then if (config.ignore_register and config.ignore_register(bufnr)) or self.buf_registered[bufnr] then
return return
end end
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 async.void(function()
if not fs.is_valid_path(path) then
return return
end end
self.database:update(path, epoch) local err, realpath = async.uv.fs_realpath(path)
if err or not realpath then
return
end
self.database:update(realpath, epoch)
self.buf_registered[bufnr] = true self.buf_registered[bufnr] = true
log.debug("registered:", bufnr, path)
end)()
end end
---@async
---@param path string ---@param path string
---@return nil ---@return nil
function Frecency:delete(path) function Frecency:delete(path)

View File

@ -1,6 +1,7 @@
local State = require "frecency.state" local State = require "frecency.state"
local Finder = require "frecency.finder" local Finder = require "frecency.finder"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs"
local fuzzy_sorter = require "frecency.fuzzy_sorter" local fuzzy_sorter = require "frecency.fuzzy_sorter"
local substr_sorter = require "frecency.substr_sorter" local substr_sorter = require "frecency.substr_sorter"
local log = require "frecency.log" local log = require "frecency.log"
@ -15,7 +16,6 @@ local uv = vim.loop or vim.uv
---@field private config FrecencyPickerConfig ---@field private config FrecencyPickerConfig
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private entry_maker FrecencyEntryMaker ---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field private lsp_workspaces string[] ---@field private lsp_workspaces string[]
---@field private namespace integer ---@field private namespace integer
---@field private recency FrecencyRecency ---@field private recency FrecencyRecency
@ -38,16 +38,14 @@ local Picker = {}
---@param database FrecencyDatabase ---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMaker ---@param entry_maker FrecencyEntryMaker
---@param fs FrecencyFS
---@param recency FrecencyRecency ---@param recency FrecencyRecency
---@param picker_config FrecencyPickerConfig ---@param picker_config FrecencyPickerConfig
---@return FrecencyPicker ---@return FrecencyPicker
Picker.new = function(database, entry_maker, fs, recency, picker_config) Picker.new = function(database, entry_maker, recency, picker_config)
local self = setmetatable({ local self = setmetatable({
config = picker_config, config = picker_config,
database = database, database = database,
entry_maker = entry_maker, entry_maker = entry_maker,
fs = fs,
lsp_workspaces = {}, lsp_workspaces = {},
namespace = vim.api.nvim_create_namespace "frecency", namespace = vim.api.nvim_create_namespace "frecency",
recency = recency, recency = recency,
@ -80,7 +78,6 @@ function Picker:finder(opts, workspace, workspace_tag)
return Finder.new( return Finder.new(
self.database, self.database,
entry_maker, entry_maker,
self.fs,
need_scandir, need_scandir,
workspace, workspace,
self.recency, self.recency,
@ -159,8 +156,8 @@ end
function Picker:default_path_display(opts, path) function Picker:default_path_display(opts, path)
local filename = Path:new(path):make_relative(opts.cwd) local filename = Path:new(path):make_relative(opts.cwd)
if not self.workspace then if not self.workspace then
if vim.startswith(filename, self.fs.os_homedir) then if vim.startswith(filename, fs.os_homedir) then
filename = "~" .. Path.path.sep .. self.fs:relative_from_home(filename) filename = "~" .. Path.path.sep .. fs.relative_from_home(filename)
elseif filename ~= path then elseif filename ~= path then
filename = "." .. Path.path.sep .. filename filename = "." .. Path.path.sep .. filename
end end
@ -269,7 +266,7 @@ function Picker:filepath_formatter(picker_opts)
for k, v in pairs(picker_opts) do for k, v in pairs(picker_opts) do
opts[k] = v opts[k] = v
end end
opts.cwd = workspace or self.fs.os_homedir opts.cwd = workspace or fs.os_homedir
return function(filename) return function(filename)
return utils.transform_path(opts, filename) return utils.transform_path(opts, filename)

View File

@ -1,27 +1,18 @@
local FS = require "frecency.fs"
local Database = require "frecency.database" local Database = require "frecency.database"
local config = require "frecency.config" local config = require "frecency.config"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] 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? local make_epoch = util.make_epoch
---@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 {}
local dir, close = util.tmpdir() local dir, close = util.tmpdir()
dir:joinpath("file_frecency.bin"):touch() dir:joinpath("file_frecency.bin"):touch()
return function() return function()
config.setup { debug = true, db_root = dir.filename } config.setup { debug = true, db_root = dir.filename }
local database = Database.new(fs) local database = Database.new()
database:start()
f(database) f(database)
close() close()
end end
@ -33,7 +24,8 @@ end
---@param epoch integer ---@param epoch integer
---@return FrecencyEntry[] ---@return FrecencyEntry[]
local function save_and_load(database, tbl, epoch) local function save_and_load(database, tbl, epoch)
database:raw_save(util.v1_table(tbl)) ---@diagnostic disable-next-line: invisible
database:raw_save(util.v1_table(tbl), database:file_lock().target)
async.util.sleep(100) async.util.sleep(100)
local entries = database:get_entries(nil, epoch) local entries = database:get_entries(nil, epoch)
table.sort(entries, function(a, b) table.sort(entries, function(a, b)

View File

@ -41,7 +41,7 @@ a.describe("file_lock", function()
with_dir(function(filename) with_dir(function(filename)
local fl = FileLock.new(filename, { retry = 1, interval = 10 }) local fl = FileLock.new(filename, { retry = 1, interval = 10 })
a.it("gets successfully", function() a.it("gets successfully", function()
local err, fd = async.uv.fs_open(fl.filename, "wx", tonumber("600", 8)) local err, fd = async.uv.fs_open(fl.lock, "wx", tonumber("600", 8))
assert.is.Nil(err) assert.is.Nil(err)
assert.is.Nil(async.uv.fs_close(fd)) assert.is.Nil(async.uv.fs_close(fd))
assert.is.Nil(fl:get()) assert.is.Nil(fl:get())
@ -131,7 +131,7 @@ a.describe("file_lock", function()
assert.are.same( assert.are.same(
"lock not found", "lock not found",
fl:with(function() fl:with(function()
assert.is.Nil(async.uv.fs_unlink(fl.filename)) assert.is.Nil(async.uv.fs_unlink(fl.lock))
return nil return nil
end) end)
) )

View File

@ -2,112 +2,14 @@
-- https://github.com/nvim-lua/plenary.nvim/blob/663246936325062427597964d81d30eaa42ab1e4/lua/plenary/test_harness.lua#L86-L86 -- https://github.com/nvim-lua/plenary.nvim/blob/663246936325062427597964d81d30eaa42ab1e4/lua/plenary/test_harness.lua#L86-L86
vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH) vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH)
---@diagnostic disable: invisible, undefined-field
local Frecency = require "frecency.klass"
local Picker = require "frecency.picker"
local util = require "frecency.tests.util" local util = require "frecency.tests.util"
local log = require "plenary.log" local log = require "plenary.log"
local Path = require "plenary.path"
local config = require "frecency.config"
---@param datetime string? local filepath = util.filepath
---@return integer local make_epoch = util.make_epoch
local function make_epoch(datetime) local make_register = util.make_register
if not datetime then local with_fake_register = util.with_fake_register
return os.time() local with_files = util.with_files
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
---@return nil
local function with_files(files, cb_or_config, callback)
local dir, close = util.make_tree(files)
local cfg
if type(cb_or_config) == "table" then
cfg = vim.tbl_extend("force", { debug = true, db_root = dir.filename }, cb_or_config)
else
cfg = { debug = true, db_root = dir.filename }
callback = cb_or_config
end
assert(callback)
log.debug(cfg)
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 })
local finder = frecency.picker:finder {}
callback(frecency, finder, dir)
close()
end
local function filepath(dir, file)
return dir:joinpath(file):absolute()
end
---@param frecency Frecency
---@param dir FrecencyPlenaryPath
---@return fun(file: string, epoch: integer, reset: boolean?): nil
local function make_register(frecency, dir)
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, epoch)
end
end
---@param frecency Frecency
---@param dir FrecencyPlenaryPath
---@param callback fun(register: fun(file: string, epoch?: integer): nil): nil
---@return nil
local function with_fake_register(frecency, dir, callback)
local bufnr = 0
local buffers = {}
local original_nvim_buf_get_name = vim.api.nvim_buf_get_name
---@diagnostic disable-next-line: redefined-local, duplicate-set-field
vim.api.nvim_buf_get_name = function(bufnr)
return buffers[bufnr]
end
---@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, epoch)
end
callback(register)
vim.api.nvim_buf_get_name = original_nvim_buf_get_name
end
---@param choice "y"|"n"
---@param callback fun(called: fun(): integer): nil
---@return nil
local function with_fake_vim_ui_select(choice, callback)
local original_vim_ui_select = vim.ui.select
local count = 0
local function called()
return count
end
---@diagnostic disable-next-line: duplicate-set-field
vim.ui.select = function(_, opts, on_choice)
count = count + 1
log.info(opts.prompt)
log.info(opts.format_item(choice))
on_choice(choice)
end
callback(called)
vim.ui.select = original_vim_ui_select
end
describe("frecency", function() describe("frecency", function()
describe("register", function() describe("register", function()
@ -116,7 +18,10 @@ describe("frecency", function()
local register = make_register(frecency, dir) local register = make_register(frecency, dir)
local epoch1 = make_epoch "2023-07-29T00: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" local epoch2 = make_epoch "2023-07-29T01:00:00+09:00"
-- HACK: This suspicious 'swapfile' setting is for avoiding E303.
vim.o.swapfile = false
register("hoge1.txt", epoch1) register("hoge1.txt", epoch1)
vim.o.swapfile = true
register("hoge2.txt", epoch2) register("hoge2.txt", epoch2)
it("has valid records in DB", function() it("has valid records in DB", function()
@ -301,208 +206,6 @@ describe("frecency", function()
end) end)
end) end)
describe("validate_database", function()
describe("when no files are unlinked", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
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, make_epoch "2023-07-29T02:00:00+09:00")
assert.are.same({
{ 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)
end)
describe("when with not force", function()
describe("when files are unlinked but it is less than threshold", function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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, 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, 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
)
end)
describe("when files are unlinked and it is more than threshold", function()
describe('when the user response "yes"', function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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()
with_fake_vim_ui_select("y", function(called)
frecency:validate_database()
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("removes entries", function()
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, timestamps = { epoch4 } },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } },
}, results)
end)
end
)
end)
describe('when the user response "no"', function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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()
with_fake_vim_ui_select("n", function(called)
frecency:validate_database()
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("removes no entries", function()
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, 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
)
end)
end)
end)
describe("when with force", 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)
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)
frecency:validate_database(true)
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("needs confirmation for removing entries", function()
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, timestamps = { epoch2 } },
}, results)
end)
end)
end)
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)
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)
frecency:validate_database(true)
it("did not call vim.ui.select()", function()
assert.are.same(0, called())
end)
end)
it("needs no confirmation for removing entries", function()
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, timestamps = { epoch2 } },
}, results)
end)
end)
end)
end)
end)
describe("delete", function() describe("delete", 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)
@ -515,8 +218,9 @@ describe("frecency", function()
it("deletes the file successfully", function() it("deletes the file successfully", function()
local path = filepath(dir, "hoge2.txt") local path = filepath(dir, "hoge2.txt")
local result local result
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field, invisible
frecency.notify = function(self, fmt, ...) frecency.notify = function(self, fmt, ...)
---@diagnostic disable-next-line: invisible
vim.notify(self:message(fmt, ...)) vim.notify(self:message(fmt, ...))
result = true result = true
end end

View File

@ -0,0 +1,258 @@
local util = require "frecency.tests.util"
local async = require "plenary.async"
local filepath = util.filepath
local make_epoch = util.make_epoch
local make_register = util.make_register
local with_fake_vim_ui_select = util.with_fake_vim_ui_select
local with_files = util.with_files
-- HACK: avoid error:
-- E5560: nvim_echo must not be called in a lua loop callback
vim.notify = function(_, _) end
describe("frecency", function()
describe("validate_database", function()
describe("when no files are unlinked", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
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, make_epoch "2023-07-29T02:00:00+09:00")
assert.are.same({
{ 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)
end)
describe("when with not force", function()
describe("when files are unlinked but it is less than threshold", function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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()
async.util.block_on(function()
frecency:validate_database()
end)
it("removes no entries", function()
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, 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
)
end)
describe("when files are unlinked and it is more than threshold", function()
describe('when the user response "yes"', function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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()
with_fake_vim_ui_select("y", function(called)
async.util.block_on(function()
frecency:validate_database()
end)
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("removes entries", function()
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, timestamps = { epoch4 } },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10, timestamps = { epoch5 } },
}, results)
end)
end
)
end)
describe('when the user response "no"', function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
{ db_validate_threshold = 3 },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
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()
with_fake_vim_ui_select("n", function(called)
async.util.block_on(function()
frecency:validate_database()
end)
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("removes no entries", function()
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, 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
)
end)
end)
end)
describe("when with force", 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)
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)
async.util.block_on(function()
frecency:validate_database(true)
end)
it("called vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("needs confirmation for removing entries", function()
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, timestamps = { epoch2 } },
}, results)
end)
end)
end)
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)
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)
async.util.block_on(function()
frecency:validate_database(true)
end)
it("did not call vim.ui.select()", function()
assert.are.same(0, called())
end)
end)
it("needs no confirmation for removing entries", function()
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, timestamps = { epoch2 } },
}, results)
end)
end)
end)
end)
describe("when case sensive filename", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
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"
register("hoge1.txt", epoch1)
register("hoge2.txt", epoch2, nil, true)
dir:joinpath("hoge1.txt"):rm()
dir:joinpath("hoge2.txt"):rename { new_name = dir:joinpath("_hoge2.txt").filename }
dir:joinpath("_hoge2.txt"):rename { new_name = dir:joinpath("Hoge2.txt").filename }
register("Hoge2.txt", epoch3)
with_fake_vim_ui_select("y", function(called)
async.util.block_on(function()
frecency:validate_database(true)
end)
it("calls vim.ui.select()", function()
assert.are.same(1, called())
end)
end)
it("removes duplicated case sensitive filenames", function()
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, timestamps = { epoch3 } },
}, results)
end)
end)
end)
end)
end)

View File

@ -1,4 +1,9 @@
---@diagnostic disable: invisible, undefined-field
local Frecency = require "frecency.klass"
local Picker = require "frecency.picker"
local config = require "frecency.config"
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
local log = require "plenary.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local Path = require "plenary.path" local Path = require "plenary.path"
local Job = require "plenary.job" local Job = require "plenary.job"
@ -7,7 +12,18 @@ local wait = require "frecency.tests.wait"
---@return FrecencyPlenaryPath ---@return FrecencyPlenaryPath
---@return fun(): nil close swwp all entries ---@return fun(): nil close swwp all entries
local function tmpdir() local function tmpdir()
local dir = Path:new(Path:new(assert(uv.fs_mkdtemp "tests_XXXXXX")):absolute()) local ci = uv.os_getenv "CI"
local dir
if ci then
dir = Path:new(assert(uv.fs_mkdtemp "tests_XXXXXX"))
else
local tmp = assert(uv.os_tmpdir())
-- HACK: plenary.path resolves paths later, so here it resolves in advance.
if uv.os_uname().sysname == "Darwin" then
tmp = tmp:gsub("^/var", "/private/var")
end
dir = Path:new(assert(uv.fs_mkdtemp(Path:new(tmp, "tests_XXXXXX").filename)))
end
return dir, function() return dir, function()
dir:rm { recursive = true } dir:rm { recursive = true }
end end
@ -37,6 +53,8 @@ local AsyncJob = async.wrap(function(cmd, callback)
end, 2) end, 2)
-- NOTE: vim.fn.strptime cannot be used in Lua loop -- NOTE: vim.fn.strptime cannot be used in Lua loop
---@param iso8601 string
---@return integer?
local function time_piece(iso8601) local function time_piece(iso8601)
local epoch local epoch
wait(function() wait(function()
@ -47,9 +65,130 @@ local function time_piece(iso8601)
return epoch return epoch
end end
---@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 time_piece(tz_fix) or 0
end
---@param records table<string, FrecencyDatabaseRecordValue> ---@param records table<string, FrecencyDatabaseRecordValue>
local function v1_table(records) local function v1_table(records)
return { version = "v1", records = records } return { version = "v1", records = records }
end end
return { make_tree = make_tree, tmpdir = tmpdir, v1_table = v1_table, time_piece = time_piece } ---@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
---@return nil
local function with_files(files, cb_or_config, callback)
local dir, close = make_tree(files)
local cfg
if type(cb_or_config) == "table" then
cfg = vim.tbl_extend("force", { debug = true, db_root = dir.filename }, cb_or_config)
else
cfg = { debug = true, db_root = dir.filename }
callback = cb_or_config
end
assert(callback)
log.debug(cfg)
config.setup(cfg)
local frecency = Frecency.new()
async.util.block_on(function()
frecency.database:start()
frecency.database.tbl:wait_ready()
end)
frecency.picker = Picker.new(frecency.database, frecency.entry_maker, frecency.recency, { editing_bufnr = 0 })
local finder = frecency.picker:finder {}
callback(frecency, finder, dir)
close()
end
local function filepath(dir, file)
return dir:joinpath(file):absolute()
end
---@param frecency Frecency
---@param dir FrecencyPlenaryPath
---@return fun(file: string, epoch: integer, reset: boolean?, wipeout?: boolean): nil reset: boolean?): nil
local function make_register(frecency, dir)
return function(file, epoch, reset, wipeout)
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, epoch)
vim.wait(1000, function()
return not not frecency.buf_registered[bufnr]
end)
-- HACK: This is needed because almost the same filenames use the same
-- buffer.
if wipeout then
vim.cmd.bwipeout()
end
end
end
---@param frecency Frecency
---@param dir FrecencyPlenaryPath
---@param callback fun(register: fun(file: string, epoch?: integer): nil): nil
---@return nil
local function with_fake_register(frecency, dir, callback)
local bufnr = 0
local buffers = {}
local original_nvim_buf_get_name = vim.api.nvim_buf_get_name
---@diagnostic disable-next-line: redefined-local, duplicate-set-field
vim.api.nvim_buf_get_name = function(bufnr)
return buffers[bufnr]
end
---@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
async.util.block_on(function()
frecency:register(bufnr, epoch)
end)
end
callback(register)
vim.api.nvim_buf_get_name = original_nvim_buf_get_name
end
---@param choice "y"|"n"
---@param callback fun(called: fun(): integer): nil
---@return nil
local function with_fake_vim_ui_select(choice, callback)
local original_vim_ui_select = vim.ui.select
local count = 0
local function called()
return count
end
---@diagnostic disable-next-line: duplicate-set-field
vim.ui.select = function(_, opts, on_choice)
count = count + 1
log.info(opts.prompt)
log.info(opts.format_item(choice))
on_choice(choice)
end
callback(called)
vim.ui.select = original_vim_ui_select
end
return {
filepath = filepath,
make_epoch = make_epoch,
make_register = make_register,
make_tree = make_tree,
tmpdir = tmpdir,
v1_table = v1_table,
with_fake_register = with_fake_register,
with_fake_vim_ui_select = with_fake_vim_ui_select,
with_files = with_files,
}

View File

@ -12,6 +12,7 @@
---@field make_relative fun(self: FrecencyPlenaryPath, cwd: string): string ---@field make_relative fun(self: FrecencyPlenaryPath, cwd: string): string
---@field parent fun(self: FrecencyPlenaryPath): FrecencyPlenaryPath ---@field parent fun(self: FrecencyPlenaryPath): FrecencyPlenaryPath
---@field path { sep: string } ---@field path { sep: string }
---@field rename fun(self: FrecencyPlenaryPath, opts: { new_name: string }): nil
---@field rm fun(self: FrecencyPlenaryPath, opts?: { recursive: boolean }): nil ---@field rm fun(self: FrecencyPlenaryPath, opts?: { recursive: boolean }): nil
---@field touch fun(self: FrecencyPlenaryPath, opts?: { parents: boolean }): nil ---@field touch fun(self: FrecencyPlenaryPath, opts?: { parents: boolean }): nil
@ -47,6 +48,11 @@ function FrecencyPlenaryAsync.run(f) end
---@class FrecencyPlenaryAsyncControlChannel ---@class FrecencyPlenaryAsyncControlChannel
---@field mpsc fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx ---@field mpsc fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx
---@field counter fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx ---@field counter fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx
local FrecencyPlenaryAsyncControlChannel
---@return fun(...): nil tx
---@return async fun(): ... rx
function FrecencyPlenaryAsyncControlChannel.oneshot() end
---@class FrecencyPlenaryAsyncControlChannelTx ---@class FrecencyPlenaryAsyncControlChannelTx
---@field send fun(entry?: any): nil ---@field send fun(entry?: any): nil
@ -92,6 +98,12 @@ function FrecencyPlenaryAsyncUv.fs_stat(path) end
---@return integer fd ---@return integer fd
function FrecencyPlenaryAsyncUv.fs_open(path, flags, mode) end function FrecencyPlenaryAsyncUv.fs_open(path, flags, mode) end
---@async
---@param path string
---@return string? err
---@return string? path
function FrecencyPlenaryAsyncUv.fs_realpath(path) end
---@async ---@async
---@param fd integer ---@param fd integer
---@param size integer ---@param size integer
@ -119,6 +131,11 @@ function FrecencyPlenaryAsyncUv.fs_unlink(path) end
---@return string? err ---@return string? err
function FrecencyPlenaryAsyncUv.fs_close(fd) end function FrecencyPlenaryAsyncUv.fs_close(fd) end
---@async
---@param async_fns (async fun(...): ...)[]
---@return table
function FrecencyPlenaryAsyncUtil.join(async_fns) end
---@async ---@async
---@param ms integer ---@param ms integer
---@return nil ---@return nil