mirror of
https://github.com/kristoferssolo/telescope-frecency.nvim.git
synced 2025-10-21 20:10:38 +00:00
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:
parent
39f70a87a2
commit
58c0089414
@ -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
|
||||
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
|
||||
|lazy.nvim| is below.
|
||||
>lua
|
||||
{
|
||||
"nvim-telescope/telescope-frecency.nvim",
|
||||
main = "frecency",
|
||||
---@type FrecencyOpts
|
||||
-- `opts` property calls the plugin's setup() function.
|
||||
-- In this case, this calls frecency.setup().
|
||||
opts = {
|
||||
db_safe_mode = false,
|
||||
},
|
||||
@ -358,7 +359,10 @@ register opened files as soon as Neovim has started. Example configuration for
|
||||
telescope.load_extension "frecency"
|
||||
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*
|
||||
|
||||
@ -26,6 +26,7 @@ local os_util = require "frecency.os_util"
|
||||
|
||||
---@class FrecencyConfig: FrecencyRawConfig
|
||||
---@field ext_config FrecencyRawConfig
|
||||
---@field private cached_ignore_regexes? string[]
|
||||
---@field private values FrecencyRawConfig
|
||||
local Config = {}
|
||||
|
||||
@ -79,6 +80,7 @@ Config.new = function()
|
||||
workspaces = true,
|
||||
}
|
||||
return setmetatable({
|
||||
cached_ignore_regexes = {},
|
||||
ext_config = {},
|
||||
values = Config.default_values,
|
||||
}, {
|
||||
@ -138,6 +140,17 @@ Config.get = function()
|
||||
return config.values
|
||||
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
|
||||
---@return nil
|
||||
Config.setup = function(ext_config)
|
||||
@ -174,6 +187,7 @@ Config.setup = function(ext_config)
|
||||
workspace_scan_cmd = { opts.workspace_scan_cmd, { "s", "t" }, true },
|
||||
workspaces = { opts.workspaces, "t" },
|
||||
}
|
||||
config.cached_ignore_regexes = nil
|
||||
config.ext_config = ext_config
|
||||
config.values = opts
|
||||
end
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
local Table = require "frecency.database.table"
|
||||
local FileLock = require "frecency.file_lock"
|
||||
local config = require "frecency.config"
|
||||
local fs = require "frecency.fs"
|
||||
local watcher = require "frecency.watcher"
|
||||
local log = require "frecency.log"
|
||||
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
@ -13,48 +14,73 @@ local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
|
||||
---@field score number
|
||||
---@field timestamps integer[]
|
||||
|
||||
---@alias FrecencyDatabaseVersion "v1"
|
||||
|
||||
---@class FrecencyDatabase
|
||||
---@field tx FrecencyPlenaryAsyncControlChannelTx
|
||||
---@field private file_lock FrecencyFileLock
|
||||
---@field private filename string
|
||||
---@field private fs FrecencyFS
|
||||
---@field private _file_lock FrecencyFileLock
|
||||
---@field private file_lock_rx async fun(): ...
|
||||
---@field private file_lock_tx fun(...): nil
|
||||
---@field private tbl FrecencyDatabaseTable
|
||||
---@field private version "v1"
|
||||
---@field private version FrecencyDatabaseVersion
|
||||
---@field private watcher_rx FrecencyPlenaryAsyncControlChannelRx
|
||||
---@field private watcher_tx FrecencyPlenaryAsyncControlChannelTx
|
||||
local Database = {}
|
||||
|
||||
---@param fs FrecencyFS
|
||||
---@return FrecencyDatabase
|
||||
Database.new = function(fs)
|
||||
Database.new = function()
|
||||
local version = "v1"
|
||||
local self = setmetatable({
|
||||
fs = fs,
|
||||
local file_lock_tx, file_lock_rx = async.control.channel.oneshot()
|
||||
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),
|
||||
version = version,
|
||||
watcher_rx = watcher_rx,
|
||||
watcher_tx = watcher_tx,
|
||||
}, { __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
|
||||
-- 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).
|
||||
local file = "file_frecency.bin"
|
||||
local db = Path.new(config.db_root, file)
|
||||
if not config.ext_config.db_root and not db:exists() then
|
||||
local old_location = Path.new(vim.fn.stdpath "data", file)
|
||||
if old_location:exists() then
|
||||
return old_location.filename
|
||||
local db = Path.new(config.db_root, file_v1).filename
|
||||
if not config.ext_config.db_root and not fs.exists(db) then
|
||||
local old_location = Path.new(vim.fn.stdpath "data", file_v1).filename
|
||||
if fs.exists(old_location) then
|
||||
return old_location
|
||||
end
|
||||
end
|
||||
return db.filename
|
||||
end)()
|
||||
self.file_lock = FileLock.new(self.filename)
|
||||
local rx
|
||||
self.tx, rx = async.control.channel.mpsc()
|
||||
self.tx.send "load"
|
||||
watcher.watch(self.filename, function()
|
||||
self.tx.send "load"
|
||||
return db
|
||||
end
|
||||
|
||||
if self.version == "v1" then
|
||||
return filename_v1()
|
||||
else
|
||||
error(("unknown version: %s"):format(self.version))
|
||||
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)
|
||||
async.void(function()
|
||||
while true do
|
||||
local mode = rx.recv()
|
||||
local mode = self.watcher_rx.recv()
|
||||
log.debug("DB coroutine start:", mode)
|
||||
if mode == "load" then
|
||||
self:load()
|
||||
@ -66,14 +92,15 @@ Database.new = function(fs)
|
||||
log.debug("DB coroutine end:", mode)
|
||||
end
|
||||
end)()
|
||||
return self
|
||||
end
|
||||
|
||||
---@async
|
||||
---@return boolean
|
||||
function Database:has_entry()
|
||||
return not vim.tbl_isempty(self.tbl.records)
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param paths string[]
|
||||
---@return nil
|
||||
function Database:insert_files(paths)
|
||||
@ -83,28 +110,32 @@ function Database:insert_files(paths)
|
||||
for _, path in ipairs(paths) do
|
||||
self.tbl.records[path] = { count = 1, timestamps = { 0 } }
|
||||
end
|
||||
self.tx.send "save"
|
||||
self.watcher_tx.send "save"
|
||||
end
|
||||
|
||||
---@async
|
||||
---@return string[]
|
||||
function Database:unlinked_entries()
|
||||
local paths = {}
|
||||
for file in pairs(self.tbl.records) do
|
||||
if not self.fs:is_valid_path(file) then
|
||||
table.insert(paths, file)
|
||||
return vim.tbl_flatten(async.util.join(vim.tbl_map(function(path)
|
||||
return function()
|
||||
local err, realpath = async.uv.fs_realpath(path)
|
||||
if err or not realpath or realpath ~= path or fs.is_ignored(realpath) then
|
||||
return path
|
||||
end
|
||||
end
|
||||
end
|
||||
return paths
|
||||
end, vim.tbl_keys(self.tbl.records))))
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param paths string[]
|
||||
function Database:remove_files(paths)
|
||||
for _, file in ipairs(paths) do
|
||||
self.tbl.records[file] = nil
|
||||
end
|
||||
self.tx.send "save"
|
||||
self.watcher_tx.send "save"
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param path string
|
||||
---@param epoch? integer
|
||||
function Database:update(path, epoch)
|
||||
@ -120,9 +151,10 @@ function Database:update(path, epoch)
|
||||
record.timestamps = new_table
|
||||
end
|
||||
self.tbl.records[path] = record
|
||||
self.tx.send "save"
|
||||
self.watcher_tx.send "save"
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param workspace? string
|
||||
---@param epoch? integer
|
||||
---@return FrecencyDatabaseEntry[]
|
||||
@ -130,7 +162,7 @@ function Database:get_entries(workspace, epoch)
|
||||
local now = epoch or os.time()
|
||||
local items = {}
|
||||
for path, record in pairs(self.tbl.records) do
|
||||
if self.fs:starts_with(path, workspace) then
|
||||
if fs.starts_with(path, workspace) then
|
||||
table.insert(items, {
|
||||
path = path,
|
||||
count = record.count,
|
||||
@ -148,13 +180,13 @@ end
|
||||
---@return nil
|
||||
function Database:load()
|
||||
local start = os.clock()
|
||||
local err, data = self.file_lock:with(function()
|
||||
local err, stat = async.uv.fs_stat(self.filename)
|
||||
local err, data = self:file_lock():with(function(target)
|
||||
local err, stat = async.uv.fs_stat(target)
|
||||
if err then
|
||||
return nil
|
||||
end
|
||||
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)
|
||||
local data
|
||||
err, data = async.uv.fs_read(fd, stat.size)
|
||||
@ -173,9 +205,9 @@ end
|
||||
---@return nil
|
||||
function Database:save()
|
||||
local start = os.clock()
|
||||
local err = self.file_lock:with(function()
|
||||
self:raw_save(self.tbl:raw())
|
||||
local err, stat = async.uv.fs_stat(self.filename)
|
||||
local err = self:file_lock():with(function(target)
|
||||
self:raw_save(self.tbl:raw(), target)
|
||||
local err, stat = async.uv.fs_stat(target)
|
||||
assert(not err, err)
|
||||
watcher.update(stat)
|
||||
return nil
|
||||
@ -185,16 +217,18 @@ function Database:save()
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param target string
|
||||
---@param tbl FrecencyDatabaseRawTable
|
||||
function Database:raw_save(tbl)
|
||||
function Database:raw_save(tbl, target)
|
||||
local f = assert(load("return " .. vim.inspect(tbl)))
|
||||
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 async.uv.fs_write(fd, data))
|
||||
assert(not async.uv.fs_close(fd))
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function Database:remove_entry(path)
|
||||
@ -202,8 +236,18 @@ function Database:remove_entry(path)
|
||||
return false
|
||||
end
|
||||
self.tbl.records[path] = nil
|
||||
self.tx.send "save"
|
||||
self.watcher_tx.send "save"
|
||||
return true
|
||||
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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
local log = require "frecency.log"
|
||||
local async = require "plenary.async"
|
||||
|
||||
---@class FrecencyDatabaseRecordValue
|
||||
---@field count integer
|
||||
@ -18,14 +19,12 @@ Table.new = function(version)
|
||||
return setmetatable({ is_ready = false, version = version }, { __index = Table.__index })
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param key string
|
||||
function Table:__index(key)
|
||||
if key == "records" and not rawget(self, "is_ready") then
|
||||
local start = os.clock()
|
||||
log.debug "waiting start"
|
||||
Table.wait_ready(self)
|
||||
log.debug(("waiting until DB become clean takes %f seconds"):format(os.clock() - start))
|
||||
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])
|
||||
end
|
||||
|
||||
@ -45,11 +44,16 @@ function Table:set(raw_table)
|
||||
end
|
||||
|
||||
---This is for internal or testing use only.
|
||||
---@async
|
||||
---@return nil
|
||||
function Table:wait_ready()
|
||||
vim.wait(2000, function()
|
||||
return rawget(self, "is_ready")
|
||||
end)
|
||||
local start = os.clock()
|
||||
local t = 0.2
|
||||
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
|
||||
|
||||
return Table
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
local WebDevicons = require "frecency.web_devicons"
|
||||
local config = require "frecency.config"
|
||||
local fs = require "frecency.fs"
|
||||
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
|
||||
local entry_display = require "telescope.pickers.entry_display" --[[@as FrecencyTelescopeEntryDisplay]]
|
||||
local utils = require "telescope.utils" --[[@as FrecencyTelescopeUtils]]
|
||||
|
||||
---@class FrecencyEntryMaker
|
||||
---@field fs FrecencyFS
|
||||
---@field loaded table<string,boolean>
|
||||
---@field web_devicons WebDevicons
|
||||
local EntryMaker = {}
|
||||
|
||||
---@param fs FrecencyFS
|
||||
---@return FrecencyEntryMaker
|
||||
EntryMaker.new = function(fs)
|
||||
return setmetatable({ fs = fs, web_devicons = WebDevicons.new() }, { __index = EntryMaker })
|
||||
EntryMaker.new = function()
|
||||
return setmetatable({ web_devicons = WebDevicons.new() }, { __index = EntryMaker })
|
||||
end
|
||||
|
||||
---@class FrecencyEntry
|
||||
@ -128,7 +127,7 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
|
||||
end
|
||||
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
|
||||
or self.fs:relative_from_home(workspace) .. Path.path.sep
|
||||
or fs.relative_from_home(workspace) .. Path.path.sep
|
||||
table.insert(items, { filtered, "Directory" })
|
||||
end
|
||||
local formatted_name, path_style = formatter(entry.name)
|
||||
@ -153,7 +152,7 @@ end
|
||||
---@return integer
|
||||
function EntryMaker:calculate_filter_column_width(workspace, workspace_tag)
|
||||
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
|
||||
|
||||
---@private
|
||||
|
||||
@ -4,23 +4,28 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
|
||||
---@class FrecencyFileLock
|
||||
---@field base string
|
||||
---@field config FrecencyFileLockConfig
|
||||
---@field filename string
|
||||
---@field config FrecencyFileLockRawConfig
|
||||
---@field lock string
|
||||
---@field target string
|
||||
local FileLock = {}
|
||||
|
||||
---@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 unlink_retry integer default: 5
|
||||
---@field interval integer default: 500
|
||||
|
||||
---@param path string
|
||||
---@param target string
|
||||
---@param file_lock_config? FrecencyFileLockConfig
|
||||
---@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 self = setmetatable({ config = config }, { __index = FileLock })
|
||||
self.filename = path .. ".lock"
|
||||
return self
|
||||
return setmetatable({ config = config, lock = target .. ".lock", target = target }, { __index = FileLock })
|
||||
end
|
||||
|
||||
---@async
|
||||
@ -31,21 +36,21 @@ function FileLock:get()
|
||||
local err, fd
|
||||
while true do
|
||||
count = count + 1
|
||||
local dir = Path.new(self.filename):parent()
|
||||
local dir = Path.new(self.lock):parent()
|
||||
if not dir:exists() then
|
||||
-- TODO: make this call be async
|
||||
log.debug(("file_lock get(): mkdir parent: %s"):format(dir.filename))
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
dir:mkdir { parents = true }
|
||||
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
|
||||
break
|
||||
end
|
||||
async.util.sleep(self.config.interval)
|
||||
if count >= self.config.retry then
|
||||
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
|
||||
log.debug("file_lock get() failed: " .. err)
|
||||
unlink_count = unlink_count + 1
|
||||
@ -67,12 +72,12 @@ end
|
||||
---@async
|
||||
---@return string? err
|
||||
function FileLock:release()
|
||||
local err = async.uv.fs_stat(self.filename)
|
||||
local err = async.uv.fs_stat(self.lock)
|
||||
if err then
|
||||
log.debug("file_lock release() not found: " .. err)
|
||||
return "lock not found"
|
||||
end
|
||||
err = async.uv.fs_unlink(self.filename)
|
||||
err = async.uv.fs_unlink(self.lock)
|
||||
if err then
|
||||
log.debug("file_lock release() unlink failed: " .. err)
|
||||
return err
|
||||
@ -81,7 +86,7 @@ end
|
||||
|
||||
---@async
|
||||
---@generic T
|
||||
---@param f fun(): T
|
||||
---@param f fun(target: string): T
|
||||
---@return string? err
|
||||
---@return T
|
||||
function FileLock:with(f)
|
||||
@ -89,7 +94,7 @@ function FileLock:with(f)
|
||||
if err then
|
||||
return err, nil
|
||||
end
|
||||
local ok, result_or_err = pcall(f)
|
||||
local ok, result_or_err = pcall(f, self.target)
|
||||
err = self:release()
|
||||
if err then
|
||||
return err, nil
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
local config = require "frecency.config"
|
||||
local fs = require "frecency.fs"
|
||||
local os_util = require "frecency.os_util"
|
||||
local log = require "frecency.log"
|
||||
local Job = require "plenary.job"
|
||||
@ -10,7 +11,6 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
---@field entries FrecencyEntry[]
|
||||
---@field scanned_entries FrecencyEntry[]
|
||||
---@field entry_maker FrecencyEntryMakerInstance
|
||||
---@field fs FrecencyFS
|
||||
---@field path? string
|
||||
---@field private database FrecencyDatabase
|
||||
---@field private rx FrecencyPlenaryAsyncControlChannelRx
|
||||
@ -32,14 +32,13 @@ local Finder = {}
|
||||
|
||||
---@param database FrecencyDatabase
|
||||
---@param entry_maker FrecencyEntryMakerInstance
|
||||
---@param fs FrecencyFS
|
||||
---@param need_scandir boolean
|
||||
---@param path string?
|
||||
---@param recency FrecencyRecency
|
||||
---@param state FrecencyState
|
||||
---@param finder_config? FrecencyFinderConfig
|
||||
---@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 scan_tx, scan_rx = async.control.channel.mpsc()
|
||||
local self = setmetatable({
|
||||
@ -47,7 +46,6 @@ Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, st
|
||||
closed = false,
|
||||
database = database,
|
||||
entry_maker = entry_maker,
|
||||
fs = fs,
|
||||
path = path,
|
||||
recency = recency,
|
||||
state = state,
|
||||
@ -168,7 +166,7 @@ end
|
||||
---@return nil
|
||||
function Finder:scan_dir_lua()
|
||||
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
|
||||
break
|
||||
end
|
||||
@ -296,25 +294,34 @@ function Finder:reflow_results()
|
||||
return
|
||||
end
|
||||
async.util.scheduler()
|
||||
local bufnr = picker.results_bufnr
|
||||
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
|
||||
return
|
||||
end
|
||||
picker:clear_extra_rows(bufnr)
|
||||
if picker.sorting_strategy == "descending" then
|
||||
local manager = picker.manager
|
||||
if not manager then
|
||||
|
||||
local function reflow()
|
||||
local bufnr = picker.results_bufnr
|
||||
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
|
||||
return
|
||||
end
|
||||
local worst_line = picker:get_row(manager:num_results())
|
||||
local wininfo = vim.fn.getwininfo(win)[1]
|
||||
local bottom = vim.api.nvim_buf_line_count(bufnr)
|
||||
if not self.reflowed or worst_line > wininfo.botline then
|
||||
self.reflowed = true
|
||||
vim.api.nvim_win_set_cursor(win, { bottom, 0 })
|
||||
picker:clear_extra_rows(bufnr)
|
||||
if picker.sorting_strategy == "descending" then
|
||||
local manager = picker.manager
|
||||
if not manager then
|
||||
return
|
||||
end
|
||||
local worst_line = picker:get_row(manager:num_results())
|
||||
local wininfo = vim.fn.getwininfo(win)[1]
|
||||
local bottom = vim.api.nvim_buf_line_count(bufnr)
|
||||
if not self.reflowed or worst_line > wininfo.botline then
|
||||
self.reflowed = true
|
||||
vim.api.nvim_win_set_cursor(win, { bottom, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.in_fast_event() then
|
||||
reflow()
|
||||
else
|
||||
vim.schedule(reflow)
|
||||
end
|
||||
end
|
||||
|
||||
return Finder
|
||||
|
||||
@ -2,57 +2,57 @@ local config = require "frecency.config"
|
||||
local os_util = require "frecency.os_util"
|
||||
local log = require "frecency.log"
|
||||
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]]
|
||||
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
local scandir = require "plenary.scandir"
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
---@class FrecencyFS
|
||||
---@field os_homedir string
|
||||
---@field private config FrecencyFSConfig
|
||||
---@field private ignore_regexes string[]
|
||||
local FS = {}
|
||||
local M = {
|
||||
os_homedir = assert(uv.os_homedir()),
|
||||
}
|
||||
|
||||
---@class FrecencyFSConfig
|
||||
---@field scan_depth integer?
|
||||
-- TODO: make this configurable
|
||||
local SCAN_DEPTH = 100
|
||||
|
||||
---@param fs_config? FrecencyFSConfig
|
||||
---@return FrecencyFS
|
||||
FS.new = function(fs_config)
|
||||
local self= setmetatable(
|
||||
{ config = vim.tbl_extend("force", { scan_depth = 100 }, fs_config or {}), os_homedir = assert(uv.os_homedir()) },
|
||||
{ __index = FS }
|
||||
)
|
||||
---@param pattern string
|
||||
self.ignore_regexes = vim.tbl_map(function(pattern)
|
||||
local regex = vim.pesc(pattern):gsub("%%%*", ".*"):gsub("%%%?", ".")
|
||||
return "^" .. regex .. "$"
|
||||
end, config.ignore_patterns)
|
||||
return self
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function M.is_ignored(path)
|
||||
for _, regex in ipairs(config.ignore_regexes()) do
|
||||
if path:find(regex) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param path? string
|
||||
---@return boolean
|
||||
function FS:is_valid_path(path)
|
||||
return not not path and Path:new(path):is_file() and not self:is_ignored(path)
|
||||
function M.is_valid_path(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
|
||||
|
||||
---@param path string
|
||||
---@return function
|
||||
function FS:scan_dir(path)
|
||||
function M.scan_dir(path)
|
||||
log.debug { path = path }
|
||||
local gitignore = self:make_gitignore(path)
|
||||
local gitignore = M.make_gitignore(path)
|
||||
return coroutine.wrap(function()
|
||||
for name, type in
|
||||
vim.fs.dir(path, {
|
||||
depth = self.config.scan_depth,
|
||||
depth = SCAN_DEPTH,
|
||||
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
|
||||
end
|
||||
end,
|
||||
})
|
||||
do
|
||||
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)
|
||||
end
|
||||
end
|
||||
@ -61,8 +61,8 @@ end
|
||||
|
||||
---@param path string
|
||||
---@return string
|
||||
function FS:relative_from_home(path)
|
||||
return Path:new(path):make_relative(self.os_homedir)
|
||||
function M.relative_from_home(path)
|
||||
return Path:new(path):make_relative(M.os_homedir)
|
||||
end
|
||||
|
||||
---@type table<string,string>
|
||||
@ -71,7 +71,7 @@ local with_sep = {}
|
||||
---@param path string
|
||||
---@param base? string
|
||||
---@return boolean
|
||||
function FS:starts_with(path, base)
|
||||
function M.starts_with(path, base)
|
||||
if not base then
|
||||
return true
|
||||
end
|
||||
@ -81,25 +81,20 @@ function FS:starts_with(path, base)
|
||||
return path:find(with_sep[base], 1, true) == 1
|
||||
end
|
||||
|
||||
---@private
|
||||
---@async
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function FS:is_ignored(path)
|
||||
for _, regex in ipairs(self.ignore_regexes) do
|
||||
if path:find(regex) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
function M.exists(path)
|
||||
return not (async.uv.fs_stat(path))
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param basepath string
|
||||
---@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 true
|
||||
end
|
||||
end
|
||||
|
||||
return FS
|
||||
return M
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
---setup() to be initialized.
|
||||
---@class FrecencyInstance
|
||||
---@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 register fun(bufnr: integer, datetime: string?): 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({}, {
|
||||
---@param self FrecencyInstance
|
||||
---@param key "complete"|"delete"|"register"|"start"|"validate_database"
|
||||
@ -28,6 +28,10 @@ local frecency = setmetatable({}, {
|
||||
end,
|
||||
})
|
||||
|
||||
local function async_call(f, ...)
|
||||
require("plenary.async").void(f)(...)
|
||||
end
|
||||
|
||||
local setup_done = false
|
||||
|
||||
---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, "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)
|
||||
frecency.validate_database(cmd_info.bang)
|
||||
async_call(frecency.validate_database, cmd_info.bang)
|
||||
end, { bang = true, desc = "Clean up DB for telescope-frecency" })
|
||||
|
||||
vim.api.nvim_create_user_command("FrecencyDelete", function(info)
|
||||
local path_string = info.args == "" and "%:p" or info.args
|
||||
---@param cmd_info FrecencyCommandInfo
|
||||
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]]
|
||||
frecency.delete(path)
|
||||
async_call(frecency.delete, path)
|
||||
end, { nargs = "?", complete = "file", desc = "Delete entry from telescope-frecency" })
|
||||
|
||||
local group = vim.api.nvim_create_augroup("TelescopeFrecency", {})
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
local Database = require "frecency.database"
|
||||
local EntryMaker = require "frecency.entry_maker"
|
||||
local FS = require "frecency.fs"
|
||||
local Picker = require "frecency.picker"
|
||||
local Recency = require "frecency.recency"
|
||||
local config = require "frecency.config"
|
||||
local fs = require "frecency.fs"
|
||||
local log = require "frecency.log"
|
||||
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
|
||||
---@class Frecency
|
||||
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
|
||||
---@field private database FrecencyDatabase
|
||||
---@field private entry_maker FrecencyEntryMaker
|
||||
---@field private fs FrecencyFS
|
||||
---@field private picker FrecencyPicker
|
||||
---@field private recency FrecencyRecency
|
||||
local Frecency = {}
|
||||
@ -18,9 +18,8 @@ local Frecency = {}
|
||||
---@return Frecency
|
||||
Frecency.new = function()
|
||||
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]]
|
||||
self.fs = FS.new()
|
||||
self.database = Database.new(self.fs)
|
||||
self.entry_maker = EntryMaker.new(self.fs)
|
||||
self.database = Database.new()
|
||||
self.entry_maker = EntryMaker.new()
|
||||
self.recency = Recency.new()
|
||||
return self
|
||||
end
|
||||
@ -28,14 +27,29 @@ end
|
||||
---This is called when `:Telescope frecency` is called at the first time.
|
||||
---@return nil
|
||||
function Frecency:setup()
|
||||
-- HACK: Wihout this wrapping, it spoils background color detection.
|
||||
-- See https://github.com/nvim-telescope/telescope-frecency.nvim/issues/210
|
||||
vim.defer_fn(function()
|
||||
local done = false
|
||||
---@async
|
||||
local function init()
|
||||
self.database:start()
|
||||
self:assert_db_entries()
|
||||
if config.auto_validate then
|
||||
self:validate_database()
|
||||
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
|
||||
|
||||
---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
|
||||
ignore_filenames = { vim.api.nvim_buf_get_name(0) }
|
||||
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(),
|
||||
ignore_filenames = ignore_filenames,
|
||||
initial_workspace_tag = opts.workspace,
|
||||
@ -69,16 +83,7 @@ function Frecency:complete(findstart, base)
|
||||
return self.picker:complete(findstart, base)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@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
|
||||
---@async
|
||||
---@param force? boolean
|
||||
---@return nil
|
||||
function Frecency:validate_database(force)
|
||||
@ -110,20 +115,38 @@ function Frecency:validate_database(force)
|
||||
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 epoch? integer
|
||||
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
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(bufnr)
|
||||
if self.buf_registered[bufnr] or not self.fs:is_valid_path(path) then
|
||||
return
|
||||
end
|
||||
self.database:update(path, epoch)
|
||||
self.buf_registered[bufnr] = true
|
||||
async.void(function()
|
||||
if not fs.is_valid_path(path) then
|
||||
return
|
||||
end
|
||||
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
|
||||
log.debug("registered:", bufnr, path)
|
||||
end)()
|
||||
end
|
||||
|
||||
---@async
|
||||
---@param path string
|
||||
---@return nil
|
||||
function Frecency:delete(path)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
local State = require "frecency.state"
|
||||
local Finder = require "frecency.finder"
|
||||
local config = require "frecency.config"
|
||||
local fs = require "frecency.fs"
|
||||
local fuzzy_sorter = require "frecency.fuzzy_sorter"
|
||||
local substr_sorter = require "frecency.substr_sorter"
|
||||
local log = require "frecency.log"
|
||||
@ -15,7 +16,6 @@ local uv = vim.loop or vim.uv
|
||||
---@field private config FrecencyPickerConfig
|
||||
---@field private database FrecencyDatabase
|
||||
---@field private entry_maker FrecencyEntryMaker
|
||||
---@field private fs FrecencyFS
|
||||
---@field private lsp_workspaces string[]
|
||||
---@field private namespace integer
|
||||
---@field private recency FrecencyRecency
|
||||
@ -38,16 +38,14 @@ local Picker = {}
|
||||
|
||||
---@param database FrecencyDatabase
|
||||
---@param entry_maker FrecencyEntryMaker
|
||||
---@param fs FrecencyFS
|
||||
---@param recency FrecencyRecency
|
||||
---@param picker_config FrecencyPickerConfig
|
||||
---@return FrecencyPicker
|
||||
Picker.new = function(database, entry_maker, fs, recency, picker_config)
|
||||
Picker.new = function(database, entry_maker, recency, picker_config)
|
||||
local self = setmetatable({
|
||||
config = picker_config,
|
||||
database = database,
|
||||
entry_maker = entry_maker,
|
||||
fs = fs,
|
||||
lsp_workspaces = {},
|
||||
namespace = vim.api.nvim_create_namespace "frecency",
|
||||
recency = recency,
|
||||
@ -80,7 +78,6 @@ function Picker:finder(opts, workspace, workspace_tag)
|
||||
return Finder.new(
|
||||
self.database,
|
||||
entry_maker,
|
||||
self.fs,
|
||||
need_scandir,
|
||||
workspace,
|
||||
self.recency,
|
||||
@ -159,8 +156,8 @@ end
|
||||
function Picker:default_path_display(opts, path)
|
||||
local filename = Path:new(path):make_relative(opts.cwd)
|
||||
if not self.workspace then
|
||||
if vim.startswith(filename, self.fs.os_homedir) then
|
||||
filename = "~" .. Path.path.sep .. self.fs:relative_from_home(filename)
|
||||
if vim.startswith(filename, fs.os_homedir) then
|
||||
filename = "~" .. Path.path.sep .. fs.relative_from_home(filename)
|
||||
elseif filename ~= path then
|
||||
filename = "." .. Path.path.sep .. filename
|
||||
end
|
||||
@ -269,7 +266,7 @@ function Picker:filepath_formatter(picker_opts)
|
||||
for k, v in pairs(picker_opts) do
|
||||
opts[k] = v
|
||||
end
|
||||
opts.cwd = workspace or self.fs.os_homedir
|
||||
opts.cwd = workspace or fs.os_homedir
|
||||
|
||||
return function(filename)
|
||||
return utils.transform_path(opts, filename)
|
||||
|
||||
@ -1,27 +1,18 @@
|
||||
local FS = require "frecency.fs"
|
||||
local Database = require "frecency.database"
|
||||
local config = require "frecency.config"
|
||||
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
local util = require "frecency.tests.util"
|
||||
async.tests.add_to_env()
|
||||
|
||||
---@param datetime string?
|
||||
---@return integer
|
||||
local function make_epoch(datetime)
|
||||
if not datetime then
|
||||
return os.time()
|
||||
end
|
||||
local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2")
|
||||
return util.time_piece(tz_fix)
|
||||
end
|
||||
local make_epoch = util.make_epoch
|
||||
|
||||
local function with_database(f)
|
||||
local fs = FS.new {}
|
||||
local dir, close = util.tmpdir()
|
||||
dir:joinpath("file_frecency.bin"):touch()
|
||||
return function()
|
||||
config.setup { debug = true, db_root = dir.filename }
|
||||
local database = Database.new(fs)
|
||||
local database = Database.new()
|
||||
database:start()
|
||||
f(database)
|
||||
close()
|
||||
end
|
||||
@ -33,7 +24,8 @@ end
|
||||
---@param epoch integer
|
||||
---@return FrecencyEntry[]
|
||||
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)
|
||||
local entries = database:get_entries(nil, epoch)
|
||||
table.sort(entries, function(a, b)
|
||||
|
||||
@ -41,7 +41,7 @@ a.describe("file_lock", function()
|
||||
with_dir(function(filename)
|
||||
local fl = FileLock.new(filename, { retry = 1, interval = 10 })
|
||||
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(async.uv.fs_close(fd))
|
||||
assert.is.Nil(fl:get())
|
||||
@ -131,7 +131,7 @@ a.describe("file_lock", function()
|
||||
assert.are.same(
|
||||
"lock not found",
|
||||
fl:with(function()
|
||||
assert.is.Nil(async.uv.fs_unlink(fl.filename))
|
||||
assert.is.Nil(async.uv.fs_unlink(fl.lock))
|
||||
return nil
|
||||
end)
|
||||
)
|
||||
|
||||
@ -2,112 +2,14 @@
|
||||
-- https://github.com/nvim-lua/plenary.nvim/blob/663246936325062427597964d81d30eaa42ab1e4/lua/plenary/test_harness.lua#L86-L86
|
||||
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 log = require "plenary.log"
|
||||
local Path = require "plenary.path"
|
||||
local config = require "frecency.config"
|
||||
|
||||
---@param datetime string?
|
||||
---@return integer
|
||||
local function make_epoch(datetime)
|
||||
if not datetime then
|
||||
return os.time()
|
||||
end
|
||||
local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2")
|
||||
return util.time_piece(tz_fix)
|
||||
end
|
||||
|
||||
---@param files string[]
|
||||
---@param cb_or_config table|fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil
|
||||
---@param callback? fun(frecency: Frecency, finder: FrecencyFinder, dir: FrecencyPlenaryPath): nil
|
||||
---@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
|
||||
local filepath = util.filepath
|
||||
local make_epoch = util.make_epoch
|
||||
local make_register = util.make_register
|
||||
local with_fake_register = util.with_fake_register
|
||||
local with_files = util.with_files
|
||||
|
||||
describe("frecency", function()
|
||||
describe("register", function()
|
||||
@ -116,7 +18,10 @@ describe("frecency", function()
|
||||
local register = make_register(frecency, dir)
|
||||
local epoch1 = make_epoch "2023-07-29T00: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)
|
||||
vim.o.swapfile = true
|
||||
register("hoge2.txt", epoch2)
|
||||
|
||||
it("has valid records in DB", function()
|
||||
@ -301,208 +206,6 @@ describe("frecency", function()
|
||||
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("when file exists", function()
|
||||
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
|
||||
@ -515,8 +218,9 @@ describe("frecency", function()
|
||||
it("deletes the file successfully", function()
|
||||
local path = filepath(dir, "hoge2.txt")
|
||||
local result
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
---@diagnostic disable-next-line: duplicate-set-field, invisible
|
||||
frecency.notify = function(self, fmt, ...)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
vim.notify(self:message(fmt, ...))
|
||||
result = true
|
||||
end
|
||||
|
||||
258
lua/frecency/tests/frecency_validate_database_spec.lua
Normal file
258
lua/frecency/tests/frecency_validate_database_spec.lua
Normal 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)
|
||||
@ -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 log = require "plenary.log"
|
||||
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
|
||||
local Path = require "plenary.path"
|
||||
local Job = require "plenary.job"
|
||||
@ -7,7 +12,18 @@ local wait = require "frecency.tests.wait"
|
||||
---@return FrecencyPlenaryPath
|
||||
---@return fun(): nil close swwp all entries
|
||||
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()
|
||||
dir:rm { recursive = true }
|
||||
end
|
||||
@ -37,6 +53,8 @@ local AsyncJob = async.wrap(function(cmd, callback)
|
||||
end, 2)
|
||||
|
||||
-- NOTE: vim.fn.strptime cannot be used in Lua loop
|
||||
---@param iso8601 string
|
||||
---@return integer?
|
||||
local function time_piece(iso8601)
|
||||
local epoch
|
||||
wait(function()
|
||||
@ -47,9 +65,130 @@ local function time_piece(iso8601)
|
||||
return epoch
|
||||
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>
|
||||
local function v1_table(records)
|
||||
return { version = "v1", records = records }
|
||||
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,
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
---@field make_relative fun(self: FrecencyPlenaryPath, cwd: string): string
|
||||
---@field parent fun(self: FrecencyPlenaryPath): FrecencyPlenaryPath
|
||||
---@field path { sep: string }
|
||||
---@field rename fun(self: FrecencyPlenaryPath, opts: { new_name: string }): nil
|
||||
---@field rm fun(self: FrecencyPlenaryPath, opts?: { recursive: boolean }): nil
|
||||
---@field touch fun(self: FrecencyPlenaryPath, opts?: { parents: boolean }): nil
|
||||
|
||||
@ -47,6 +48,11 @@ function FrecencyPlenaryAsync.run(f) end
|
||||
---@class FrecencyPlenaryAsyncControlChannel
|
||||
---@field mpsc fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx
|
||||
---@field counter fun(): FrecencyPlenaryAsyncControlChannelTx, FrecencyPlenaryAsyncControlChannelRx
|
||||
local FrecencyPlenaryAsyncControlChannel
|
||||
|
||||
---@return fun(...): nil tx
|
||||
---@return async fun(): ... rx
|
||||
function FrecencyPlenaryAsyncControlChannel.oneshot() end
|
||||
|
||||
---@class FrecencyPlenaryAsyncControlChannelTx
|
||||
---@field send fun(entry?: any): nil
|
||||
@ -92,6 +98,12 @@ function FrecencyPlenaryAsyncUv.fs_stat(path) end
|
||||
---@return integer fd
|
||||
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
|
||||
---@param fd integer
|
||||
---@param size integer
|
||||
@ -119,6 +131,11 @@ function FrecencyPlenaryAsyncUv.fs_unlink(path) end
|
||||
---@return string? err
|
||||
function FrecencyPlenaryAsyncUv.fs_close(fd) end
|
||||
|
||||
---@async
|
||||
---@param async_fns (async fun(...): ...)[]
|
||||
---@return table
|
||||
function FrecencyPlenaryAsyncUtil.join(async_fns) end
|
||||
|
||||
---@async
|
||||
---@param ms integer
|
||||
---@return nil
|
||||
|
||||
Loading…
Reference in New Issue
Block a user