feat: check DB file has been changed (#143)

* refactor: unite logic for finder & async_finder

* chore: fix types

* chore: add sleep to show results at first

* refactor: fix to find results separatedly

* test: remove unnecessary ones and fix others

* test: add matrix for 0.9.x & Windows

* test: use forked plenary.log for Windows

* test: fix to use strptime in Windows

* test: run again if segmentation fault in Windows

* test: loosen timeout for Perl

* test: use the latest plenary.nvim again

* chore: fix types

* chore: change variable name

* feat: watch changes of DB to reload

* chore: add comments to steps

* test: copy whole modules for testing in Windows

* fix: make valid paths for Windows

* test: add tests for Native

* test: use robust way to calculate time

vim.fn.strptime cannot be used in Lua loop

* chore: fix comments

* refactor: simplify the code

* test: loosen condition to detect failures

* test: disable some logging

Many loggings make the test fail.

* test: run tests sequentially in Windows

* test: loosen timeout not to fail on Windows
This commit is contained in:
JINNOUCHI Yasushi 2023-09-17 15:21:01 +09:00 committed by GitHub
parent fbda5d91d6
commit 767fbf074f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 604 additions and 454 deletions

View File

@ -9,11 +9,11 @@ jobs:
matrix:
os:
- ubuntu-latest
# TODO: nix seems not to work with SIP
# - macos-latest
# TODO: PlenaryBustedDirectory seems not to run on Windows
# - windows-latest
- macos-latest
- windows-latest
version:
- v0.9.2
- v0.9.1
- v0.9.0
- nightly
runs-on: ${{ matrix.os }}
@ -41,7 +41,7 @@ jobs:
with:
neovim: true
version: ${{ matrix.version }}
- name: Run tests
- name: Run tests (not for Windows)
env:
PLENARY_PATH: plenary.nvim
TELESCOPE_PATH: telescope.nvim
@ -53,8 +53,29 @@ jobs:
MINIMAL_LUA=${TEST_DIR}minimal.lua
NVIM=$(perl -e '$_ = $ENV{EXE}; s,\\,/,g; print')
$NVIM --headless --clean -u $MINIMAL_LUA -c "PlenaryBustedDirectory $TEST_DIR {minimal_init = '$MINIMAL_LUA'}"
if: matrix.os != 'windows-latest'
- name: Run tests (for Windows)
shell: bash
env:
PLENARY_PATH: plenary.nvim
TELESCOPE_PATH: telescope.nvim
SQLITE_PATH: sqlite.lua
DEBUG_PLENARY: 1
EXE: ${{ steps.nvim.outputs.executable }}
run: |-
# HACK: This is needed because it fails to add runtimepath's.
cp -af $PLENARY_PATH/lua/plenary/ lua/
cp -af $TELESCOPE_PATH/lua/telescope/ lua/
cp -af $SQLITE_PATH/lua/sqlite/ lua/
TEST_DIR=lua/frecency/tests/
MINIMAL_LUA=${TEST_DIR}minimal.lua
NVIM=$(perl -e '$_ = $ENV{EXE}; s,\\,/,g; print')
$NVIM --headless --clean -u $MINIMAL_LUA -c "PlenaryBustedDirectory $TEST_DIR {minimal_init = '$MINIMAL_LUA', timeout = 180000, sequential = true}"
if: matrix.os == 'windows-latest'
- name: Type Check Code Base
uses: mrcjkb/lua-typecheck-action@v0.2.0
with:
checkLevel: Hint
configpath: .luarc.json
# NOTE: This step needs nix that seems not to work with SIP (macOS)
if: matrix.os == 'ubuntu-latest'

View File

@ -1,123 +0,0 @@
local async = require "plenary.async" --[[@as PlenaryAsync]]
---@class FrecencyAsyncFinder
---@field closed boolean
---@field entries FrecencyEntry[]
---@field reflowed boolean
---@field rx PlenaryAsyncControlChannelRx
---@field state FrecencyState
---@overload fun(_: string, process_result: (fun(entry: FrecencyEntry): nil), process_complete: fun(): nil): nil
local AsyncFinder = {}
---@param fs FrecencyFS
---@param state FrecencyState
---@param path string
---@param entry_maker fun(file: FrecencyFile): FrecencyEntry
---@param initial_results FrecencyFile[]
---@return FrecencyAsyncFinder
AsyncFinder.new = function(state, fs, path, entry_maker, initial_results)
local self = setmetatable({ closed = false, entries = {}, reflowed = false, state = state }, {
__index = AsyncFinder,
---@param self FrecencyAsyncFinder
__call = function(self, ...)
return self:find(...)
end,
})
local seen = {}
for i, file in ipairs(initial_results) do
local entry = entry_maker(file)
seen[entry.filename] = true
entry.index = i
table.insert(self.entries, entry)
end
local tx, rx = async.control.channel.mpsc()
self.rx = rx
async.run(function()
local index = #initial_results
local count = 0
for name in fs:scan_dir(path) do
if self.closed then
break
end
local fullpath = fs.joinpath(path, name)
if not seen[fullpath] then
seen[fullpath] = true
index = index + 1
count = count + 1
local entry = entry_maker { id = 0, count = 0, path = fs.joinpath(path, name), score = 0 }
if entry then
entry.index = index
table.insert(self.entries, entry)
tx.send(entry)
if count % 1000 == 0 then
self:reflow_results()
-- NOTE: This is needed not to lock text input.
async.util.sleep(50)
end
end
end
end
self:close()
tx.send(nil)
end)
return self
end
---@param _ string
---@param process_result fun(entry: FrecencyEntry): nil
---@param process_complete fun(): nil
---@return nil
function AsyncFinder:find(_, process_result, process_complete)
for _, entry in ipairs(self.entries) do
if process_result(entry) then
return
end
end
local last_index = self.entries[#self.entries].index
while true do
if self.closed then
break
end
local entry = self.rx.recv()
if not entry then
break
elseif entry.index > last_index and process_result(entry) then
return
end
end
process_complete()
end
function AsyncFinder:close()
self.closed = true
end
---@return nil
function AsyncFinder:reflow_results()
local picker = self.state:get()
if not picker then
return
end
local bufnr = picker.results_bufnr
local win = picker.results_win
if not bufnr or not win then
return
end
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())
---@type WinInfo
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
return AsyncFinder

View File

@ -1,7 +1,9 @@
local FileLock = require "frecency.file_lock"
local wait = require "frecency.wait"
local watcher = require "frecency.database.native.watcher"
local log = require "plenary.log"
local async = require "plenary.async" --[[@as PlenaryAsync]]
local Path = require "plenary.path" --[[@as PlenaryPath]]
---@class FrecencyDatabaseNative: FrecencyDatabase
---@field version "v1"
@ -29,11 +31,20 @@ Native.new = function(fs, config)
table = { version = version, records = {} },
version = version,
}, { __index = Native })
self.filename = self.config.root .. "/file_frecency.bin"
self.filename = Path.new(self.config.root, "file_frecency.bin").filename
self.file_lock = FileLock.new(self.filename)
local tx, rx = async.control.channel.counter()
watcher.watch(self.filename, tx)
wait(function()
self:load()
end)
async.void(function()
while true do
rx.last()
log.debug "file changed. loading..."
self:load()
end
end)()
return self
end
@ -102,8 +113,6 @@ end
---@param datetime string?
---@return FrecencyDatabaseEntry[]
function Native:get_entries(workspace, datetime)
-- TODO: check mtime of DB and reload it
-- self:load()
local now = self:now(datetime)
local items = {}
for path, record in pairs(self.table.records) do
@ -120,11 +129,21 @@ function Native:get_entries(workspace, datetime)
return items
end
-- TODO: remove this func
-- This is a func for testing
---@private
---@param datetime string?
---@return integer
function Native:now(datetime)
return datetime and vim.fn.strptime("%FT%T%z", datetime) or os.time()
if not datetime then
return os.time()
end
local epoch
wait(function()
local tz_fix = datetime:gsub("+(%d%d):(%d%d)$", "+%1%2")
epoch = require("frecency.tests.util").time_piece(tz_fix)
end)
return epoch
end
---@async
@ -132,17 +151,18 @@ end
function Native:load()
local start = os.clock()
local err, data = self.file_lock:with(function()
local err, st = async.uv.fs_stat(self.filename)
local err, stat = async.uv.fs_stat(self.filename)
if err then
return nil
end
local fd
err, fd = async.uv.fs_open(self.filename, "r", tonumber("644", 8))
assert(not err)
assert(not err, err)
local data
err, data = async.uv.fs_read(fd, st.size)
assert(not err)
err, data = async.uv.fs_read(fd, stat.size)
assert(not err, err)
assert(not async.uv.fs_close(fd))
watcher.update(stat)
return data
end)
assert(not err, err)
@ -158,16 +178,23 @@ end
function Native:save()
local start = os.clock()
local err = self.file_lock:with(function()
local f = assert(load("return " .. vim.inspect(self.table)))
local data = string.dump(f)
local err, fd = async.uv.fs_open(self.filename, "w", tonumber("644", 8))
assert(not err)
assert(not async.uv.fs_write(fd, data))
assert(not async.uv.fs_close(fd))
self:raw_save(self.table)
local err, stat = async.uv.fs_stat(self.filename)
assert(not err, err)
watcher.update(stat)
return nil
end)
assert(not err, err)
log.debug(("save() takes %f seconds"):format(os.clock() - start))
end
function Native:raw_save(tbl)
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))
assert(not err, err)
assert(not async.uv.fs_write(fd, data))
assert(not async.uv.fs_close(fd))
end
return Native

View File

@ -0,0 +1,87 @@
local async = require "plenary.async" --[[@as PlenaryAsync]]
local log = require "plenary.log"
local uv = vim.loop or vim.uv
---@class FrecencyNativeWatcherMtime
---@field sec integer
---@field nsec integer
local Mtime = {}
---@param mtime FsStatMtime
---@return FrecencyNativeWatcherMtime
Mtime.new = function(mtime)
return setmetatable({ sec = mtime.sec, nsec = mtime.nsec }, Mtime)
end
---@param other FrecencyNativeWatcherMtime
---@return boolean
function Mtime:__eq(other)
return self.sec == other.sec and self.nsec == other.nsec
end
---@return string
function Mtime:__tostring()
return string.format("%d.%d", self.sec, self.nsec)
end
---@class FrecencyNativeWatcher
---@field handler UvFsEventHandle
---@field path string
---@field mtime FrecencyNativeWatcherMtime
local Watcher = {}
---@return FrecencyNativeWatcher
Watcher.new = function()
return setmetatable({ path = "", mtime = Mtime.new { sec = 0, nsec = 0 } }, { __index = Watcher })
end
---@param path string
---@param tx PlenaryAsyncControlChannelTx
function Watcher:watch(path, tx)
if self.handler then
self.handler:stop()
end
self.handler = assert(uv.new_fs_event()) --[[@as UvFsEventHandle]]
self.handler:start(path, { recursive = true }, function(err, _, _)
if err then
log.debug("failed to watch path: " .. err)
return
end
async.void(function()
-- NOTE: wait for updating mtime
async.util.sleep(50)
local stat
err, stat = async.uv.fs_stat(path)
if err then
log.debug("failed to stat path: " .. err)
return
end
local mtime = Mtime.new(stat.mtime)
if self.mtime ~= mtime then
log.debug(("mtime changed: %s -> %s"):format(self.mtime, mtime))
self.mtime = mtime
tx.send()
end
end)()
end)
end
local watcher = Watcher.new()
return {
---@param path string
---@param tx PlenaryAsyncControlChannelTx
---@return nil
watch = function(path, tx)
log.debug("watch path: " .. path)
watcher:watch(path, tx)
end,
---@param stat FsStat
---@return nil
update = function(stat)
local mtime = Mtime.new(stat.mtime)
log.debug(("update mtime: %s -> %s"):format(watcher.mtime, mtime))
watcher.mtime = mtime
end,
}

View File

@ -40,10 +40,12 @@ end
---@field score number
---@field display fun(entry: FrecencyEntry): string, table
---@alias FrecencyEntryMakerInstance fun(file: FrecencyFile): FrecencyEntry
---@param filepath_formatter FrecencyFilepathFormatter
---@param workspace string?
---@param workspace_tag string?
---@return fun(file: FrecencyFile): FrecencyEntry
---@return FrecencyEntryMakerInstance
function EntryMaker:create(filepath_formatter, workspace, workspace_tag)
local displayer = entry_display.create {
separator = "",

View File

@ -1,47 +1,191 @@
local AsyncFinder = require "frecency.async_finder"
local finders = require "telescope.finders"
local async = require "plenary.async" --[[@as PlenaryAsync]]
local log = require "plenary.log"
---@class FrecencyFinder
---@field private config FrecencyFinderConfig
---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field config FrecencyFinderConfig
---@field closed boolean
---@field entries FrecencyEntry[]
---@field entry_maker FrecencyEntryMakerInstance
---@field fs FrecencyFS
---@field need_scandir boolean
---@field path string?
---@field private database FrecencyDatabase
---@field private recency FrecencyRecency
---@field private rx PlenaryAsyncControlChannelRx
---@field private state FrecencyState
---@field private tx PlenaryAsyncControlChannelTx
local Finder = {}
---@class FrecencyFinderConfig
---@field chunk_size integer
---@field chunk_size integer default: 1000
---@field sleep_interval integer default: 50
---@param entry_maker FrecencyEntryMaker
---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMakerInstance
---@param fs FrecencyFS
---@param need_scandir boolean
---@param path string?
---@param recency FrecencyRecency
---@param state FrecencyState
---@param config FrecencyFinderConfig?
---@return FrecencyFinder
Finder.new = function(entry_maker, fs, config)
return setmetatable(
{ config = vim.tbl_extend("force", { chunk_size = 1000 }, config or {}), entry_maker = entry_maker, fs = fs },
{ __index = Finder }
)
Finder.new = function(database, entry_maker, fs, need_scandir, path, recency, state, config)
local tx, rx = async.control.channel.mpsc()
return setmetatable({
config = vim.tbl_extend("force", { chunk_size = 1000, sleep_interval = 50 }, config or {}),
closed = false,
database = database,
entries = {},
entry_maker = entry_maker,
fs = fs,
need_scandir = need_scandir,
path = path,
recency = recency,
rx = rx,
state = state,
tx = tx,
}, {
__index = Finder,
---@param self FrecencyFinder
__call = function(self, ...)
return self:find(...)
end,
})
end
---@class FrecencyFinderOptions
---@field need_scandir boolean
---@field workspace string?
---@field workspace_tag string?
---@param datetime string?
---@return nil
function Finder:start(datetime)
async.void(function()
-- NOTE: return to the main loop to show the main window
async.util.sleep(0)
local seen = {}
for i, file in ipairs(self:get_results(self.path, datetime)) do
local entry = self.entry_maker(file)
seen[entry.filename] = true
entry.index = i
table.insert(self.entries, entry)
self.tx.send(entry)
end
if self.need_scandir and self.path then
-- NOTE: return to the main loop to show results from DB
async.util.sleep(self.config.sleep_interval)
self:scan_dir(seen)
end
self:close()
self.tx.send(nil)
end)()
end
---@param state FrecencyState
---@param filepath_formatter FrecencyFilepathFormatter
---@param initial_results table
---@param opts FrecencyFinderOptions
---@return table
function Finder:start(state, filepath_formatter, initial_results, opts)
local entry_maker = self.entry_maker:create(filepath_formatter, opts.workspace, opts.workspace_tag)
if not opts.need_scandir then
return finders.new_table {
results = initial_results,
entry_maker = entry_maker,
}
---@param seen table<string, boolean>
---@return nil
function Finder:scan_dir(seen)
local count = 0
local index = #self.entries
for name in self.fs:scan_dir(self.path) do
if self.closed then
break
end
local fullpath = self.fs.joinpath(self.path, name)
if not seen[fullpath] then
seen[fullpath] = true
count = count + 1
local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 }
if entry then
index = index + 1
entry.index = index
table.insert(self.entries, entry)
self.tx.send(entry)
if count % self.config.chunk_size == 0 then
self:reflow_results()
async.util.sleep(self.config.sleep_interval)
end
end
end
end
end
---@param _ string
---@param process_result fun(entry: FrecencyEntry): nil
---@param process_complete fun(): nil
---@return nil
function Finder:find(_, process_result, process_complete)
local index = 0
for _, entry in ipairs(self.entries) do
index = index + 1
if process_result(entry) then
return
end
end
local count = 0
while not self.closed do
count = count + 1
local entry = self.rx.recv()
if not entry then
break
elseif entry.index > index and process_result(entry) then
return
end
end
process_complete()
end
---@param workspace string?
---@param datetime string?
---@return FrecencyFile[]
function Finder:get_results(workspace, datetime)
log.debug { workspace = workspace or "NONE" }
local start_fetch = os.clock()
local files = self.database:get_entries(workspace, datetime)
log.debug(("it takes %f seconds in fetching entries"):format(os.clock() - start_fetch))
local start_results = os.clock()
local elapsed_recency = 0
for _, file in ipairs(files) do
local start_recency = os.clock()
file.score = file.ages and self.recency:calculate(file.count, file.ages) or 0
file.ages = nil
elapsed_recency = elapsed_recency + (os.clock() - start_recency)
end
log.debug(("it takes %f seconds in calculating recency"):format(elapsed_recency))
log.debug(("it takes %f seconds in making results"):format(os.clock() - start_results))
local start_sort = os.clock()
table.sort(files, function(a, b)
return a.score > b.score or (a.score == b.score and a.path > b.path)
end)
log.debug(("it takes %f seconds in sorting"):format(os.clock() - start_sort))
return files
end
function Finder:close()
self.closed = true
end
function Finder:reflow_results()
local picker = self.state:get()
if not picker then
return
end
local bufnr = picker.results_bufnr
local win = picker.results_win
if not bufnr or not win then
return
end
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())
---@type WinInfo
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
log.debug { finder = opts }
return AsyncFinder.new(state, self.fs, opts.workspace, entry_maker, initial_results)
end
return Finder

View File

@ -2,7 +2,6 @@ local Sqlite = require "frecency.database.sqlite"
local Native = require "frecency.database.native"
local EntryMaker = require "frecency.entry_maker"
local FS = require "frecency.fs"
local Finder = require "frecency.finder"
local Migrator = require "frecency.migrator"
local Picker = require "frecency.picker"
local Recency = require "frecency.recency"
@ -14,7 +13,7 @@ local log = require "plenary.log"
---@field config FrecencyConfig
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
---@field private database FrecencyDatabase
---@field private finder FrecencyFinder
---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field private migrator FrecencyMigrator
---@field private picker FrecencyPicker
@ -69,11 +68,10 @@ Frecency.new = function(opts)
end
self.database = Database.new(self.fs, { root = config.db_root })
local web_devicons = WebDevicons.new(not config.disable_devicons)
local entry_maker = EntryMaker.new(self.fs, web_devicons, {
self.entry_maker = EntryMaker.new(self.fs, web_devicons, {
show_filter_column = config.show_filter_column,
show_scores = config.show_scores,
})
self.finder = Finder.new(entry_maker, self.fs)
self.recency = Recency.new()
self.migrator = Migrator.new(self.fs, self.recency, self.config.db_root)
return self
@ -122,7 +120,7 @@ function Frecency:start(opts)
local start = os.clock()
log.debug "Frecency:start"
opts = opts or {}
self.picker = Picker.new(self.database, self.finder, self.fs, self.recency, {
self.picker = Picker.new(self.database, self.entry_maker, self.fs, self.recency, {
default_workspace_tag = self.config.default_workspace,
editing_bufnr = vim.api.nvim_get_current_buf(),
filter_delimiter = self.config.filter_delimiter,

View File

@ -1,4 +1,5 @@
local State = require "frecency.state"
local Finder = require "frecency.finder"
local log = require "plenary.log"
local Path = require "plenary.path" --[[@as PlenaryPath]]
local actions = require "telescope.actions"
@ -11,12 +12,12 @@ local uv = vim.loop or vim.uv
---@class FrecencyPicker
---@field private config FrecencyPickerConfig
---@field private database FrecencyDatabase
---@field private finder FrecencyFinder
---@field private entry_maker FrecencyEntryMaker
---@field private fs FrecencyFS
---@field private lsp_workspaces string[]
---@field private namespace integer
---@field private recency FrecencyRecency
---@field private results table[]
---@field private state FrecencyState
---@field private workspace string?
---@field private workspace_tag_regex string
local Picker = {}
@ -37,21 +38,20 @@ local Picker = {}
---@field score number
---@param database FrecencyDatabase
---@param finder FrecencyFinder
---@param entry_maker FrecencyEntryMaker
---@param fs FrecencyFS
---@param recency FrecencyRecency
---@param config FrecencyPickerConfig
---@return FrecencyPicker
Picker.new = function(database, finder, fs, recency, config)
Picker.new = function(database, entry_maker, fs, recency, config)
local self = setmetatable({
config = config,
database = database,
finder = finder,
entry_maker = entry_maker,
fs = fs,
lsp_workspaces = {},
namespace = vim.api.nvim_create_namespace "frecency",
recency = recency,
results = {},
}, { __index = Picker })
local d = self.config.filter_delimiter
self.workspace_tag_regex = "^%s*" .. d .. "(%S+)" .. d
@ -70,6 +70,16 @@ end
---| fun(opts: FrecencyPickerOptions, path: string): string
---@field workspace string?
---@param opts table
---@param workspace string?
---@param workspace_tag string?
function Picker:finder(opts, workspace, workspace_tag)
local filepath_formatter = self:filepath_formatter(opts)
local entry_maker = self.entry_maker:create(filepath_formatter, workspace, workspace_tag)
local need_scandir = not not (workspace and self.config.show_unindexed)
return Finder.new(self.database, entry_maker, self.fs, need_scandir, workspace, self.recency, self.state)
end
---@param opts FrecencyPickerOptions?
function Picker:start(opts)
opts = vim.tbl_extend("force", {
@ -80,36 +90,25 @@ function Picker:start(opts)
}, opts or {}) --[[@as FrecencyPickerOptions]]
self.workspace = self:get_workspace(opts.cwd, self.config.initial_workspace_tag)
log.debug { workspace = self.workspace }
self.results = self:fetch_results(self.workspace)
local state = State.new()
local filepath_formatter = self:filepath_formatter(opts)
local finder = self.finder:start(state, filepath_formatter, self.results, {
need_scandir = self.workspace and self.config.show_unindexed and true or false,
workspace = self.workspace,
workspace_tag = self.config.initial_workspace_tag,
})
self.state = State.new()
local finder = self:finder(opts, self.workspace, self.config.initial_workspace_tag)
local picker = pickers.new(opts, {
prompt_title = "Frecency",
finder = finder,
previewer = config_values.file_previewer(opts),
sorter = sorters.get_substr_matcher(),
on_input_filter_cb = self:on_input_filter_cb(state, opts),
on_input_filter_cb = self:on_input_filter_cb(opts),
attach_mappings = function(prompt_bufnr)
return self:attach_mappings(prompt_bufnr)
end,
})
state:set(picker)
self.state:set(picker)
picker:find()
finder:start()
self:set_prompt_options(picker.prompt_bufnr)
end
function Picker:discard_results()
-- TODO: implement here when it needs to cache.
end
--- See :h 'complete-functions'
---@param findstart 1|0
---@param base string
@ -178,34 +177,6 @@ function Picker:get_workspace(cwd, tag)
end
end
---@private
---@param workspace string?
---@param datetime string? ISO8601 format string
---@return FrecencyFile[]
function Picker:fetch_results(workspace, datetime)
log.debug { workspace = workspace or "NONE" }
local start_fetch = os.clock()
local files = self.database:get_entries(workspace, datetime)
log.debug(("it takes %f seconds in fetching entries"):format(os.clock() - start_fetch))
local start_results = os.clock()
local elapsed_recency = 0
for _, file in ipairs(files) do
local start_recency = os.clock()
file.score = file.ages and self.recency:calculate(file.count, file.ages) or 0
file.ages = nil
elapsed_recency = elapsed_recency + (os.clock() - start_recency)
end
log.debug(("it takes %f seconds in calculating recency"):format(elapsed_recency))
log.debug(("it takes %f seconds in making results"):format(os.clock() - start_results))
local start_sort = os.clock()
table.sort(files, function(a, b)
return a.score > b.score or (a.score == b.score and a.path > b.path)
end)
log.debug(("it takes %f seconds in sorting"):format(os.clock() - start_sort))
return files
end
---@private
---@return string?
function Picker:get_lsp_workspace()
@ -216,11 +187,9 @@ function Picker:get_lsp_workspace()
end
---@private
---@param state FrecencyState
---@param picker_opts table
---@return fun(prompt: string): table
function Picker:on_input_filter_cb(state, picker_opts)
local filepath_formatter = self:filepath_formatter(picker_opts)
function Picker:on_input_filter_cb(picker_opts)
return function(prompt)
local workspace
local start, finish, tag = prompt:find(self.workspace_tag_regex)
@ -230,7 +199,7 @@ function Picker:on_input_filter_cb(state, picker_opts)
else
workspace = self:get_workspace(picker_opts.cwd, tag) or self.workspace
end
local picker = state:get()
local picker = self.state:get()
if picker then
local buf = picker.prompt_bufnr
vim.api.nvim_buf_clear_namespace(buf, self.namespace, 0, -1)
@ -249,13 +218,7 @@ function Picker:on_input_filter_cb(state, picker_opts)
end
if self.workspace ~= workspace then
self.workspace = workspace
self.results = self:fetch_results(workspace)
opts.updated_finder = self.finder:start(state, filepath_formatter, self.results, {
initial_results = self.results,
need_scandir = self.workspace and self.config.show_unindexed and true or false,
workspace = self.workspace,
workspace_tag = tag,
})
opts.updated_finder = self:finder(picker_opts, self.workspace, tag or self.config.initial_workspace_tag):start()
end
return opts
end

View File

@ -1,98 +0,0 @@
---@diagnostic disable: invisible
local AsyncFinder = require "frecency.async_finder"
local State = require "frecency.state"
local FS = require "frecency.fs"
local EntryMaker = require "frecency.entry_maker"
local WebDevicons = require "frecency.web_devicons"
local util = require "frecency.tests.util"
---@param files string[]
---@param initial_results string[]
---@param callback fun(async_finder: FrecencyAsyncFinder, dir: PlenaryPath): nil
local function with_files(files, initial_results, callback)
local dir, close = util.make_tree(files)
local fs = FS.new { ignore_patterns = {} }
local web_devicons = WebDevicons.new(true)
local function filepath_formatter()
return function(name)
return name
end
end
local entry_maker = EntryMaker.new(fs, web_devicons, { show_filter_column = false, show_scores = false })
:create(filepath_formatter, dir:absolute())
local initials = vim.tbl_map(function(v)
return { path = (dir / v):absolute() }
end, initial_results)
local async_finder = AsyncFinder.new(State.new(), fs, dir:absolute(), entry_maker, initials)
callback(async_finder, dir)
close()
end
describe("async_finder", function()
local function run(async_finder)
local count = { process_result = 0, process_complete = 0 }
local results = {}
async_finder("", function(result)
count.process_result = count.process_result + 1
table.insert(results, result.filename)
end, function()
count.process_complete = count.process_complete + 1
end)
return count, results
end
describe("with no initial_results", function()
with_files({ "hoge1.txt", "hoge2.txt" }, {}, function(async_finder, dir)
describe("when run at the first time", function()
local count, results = run(async_finder)
it("called process_result() at 2 times", function()
assert.are.same(2, count.process_result)
end)
it("called process_complete() at 1 time", function()
assert.are.same(1, count.process_complete)
end)
it("returns the whole results", function()
assert.are.same({
dir:joinpath("hoge1.txt").filename,
dir:joinpath("hoge2.txt").filename,
}, results)
end)
end)
describe("when run again", function()
local count, results = run(async_finder)
it("called process_result() at 2 times", function()
assert.are.same(2, count.process_result)
end)
it("called process_complete() at 1 time", function()
assert.are.same(1, count.process_complete)
end)
it("returns the same results", function()
assert.are.same({
dir:joinpath("hoge1.txt").filename,
dir:joinpath("hoge2.txt").filename,
}, results)
end)
end)
end)
end)
describe("with initial_results", function()
with_files({ "fuga1.txt", "hoge1.txt", "hoge2.txt" }, { "fuga1.txt" }, function(async_finder, dir)
local count, results = run(async_finder)
it("called process_result() at 3 times", function()
assert.are.same(3, count.process_result)
end)
it("called process_complete() at 1 time", function()
assert.are.same(1, count.process_complete)
end)
it("returns the same results without duplications", function()
assert.are.same({
dir:joinpath("fuga1.txt").filename,
dir:joinpath("hoge1.txt").filename,
dir:joinpath("hoge2.txt").filename,
}, results)
end)
end)
end)
end)

View File

@ -8,7 +8,7 @@ local Path = require "plenary.path"
local use_sqlite
---@param files string[]
---@param callback fun(frecency: Frecency, dir: PlenaryPath): nil
---@param callback fun(frecency: Frecency, finder: FrecencyFinder, dir: PlenaryPath): nil
---@return nil
local function with_files(files, callback)
local dir, close = util.make_tree(files)
@ -16,12 +16,13 @@ local function with_files(files, callback)
local frecency = Frecency.new { db_root = dir.filename, use_sqlite = use_sqlite }
frecency.picker = Picker.new(
frecency.database,
frecency.finder,
frecency.entry_maker,
frecency.fs,
frecency.recency,
{ editing_bufnr = 0, filter_delimiter = ":", show_unindexed = false, workspaces = {} }
)
callback(frecency, dir)
local finder = frecency.picker:finder {}
callback(frecency, finder, dir)
close()
end
@ -92,13 +93,13 @@ describe("frecency", function()
describe(db, function()
describe("register", function()
describe("when opening files", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T01:00:00+09:00")
it("has valid records in DB", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00")
assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 },
@ -108,14 +109,14 @@ describe("frecency", function()
end)
describe("when opening again", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T01:00:00+09:00")
register("hoge1.txt", "2023-07-29T02:00:00+09:00", true)
it("increases the score", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T03:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T03:00:00+09:00")
assert.are.same({
{ count = 2, path = filepath(dir, "hoge1.txt"), score = 40 },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
@ -125,14 +126,14 @@ describe("frecency", function()
end)
describe("when opening again but the same instance", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T01:00:00+09:00")
register("hoge1.txt", "2023-07-29T02:00:00+09:00")
it("does not increase the score", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T03:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T03:00:00+09:00")
assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 },
@ -142,7 +143,7 @@ describe("frecency", function()
end)
describe("when opening more than 10 times", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge1.txt", "2023-07-29T00:01:00+09:00", true)
@ -161,7 +162,7 @@ describe("frecency", function()
register("hoge2.txt", "2023-07-29T00:11:00+09:00", true)
it("calculates score from the recent 10 times", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T00:12:00+09:00")
local results = finder:get_results(nil, "2023-07-29T00:12:00+09:00")
assert.are.same({
{ count = 12, path = filepath(dir, "hoge2.txt"), score = 12 * (10 * 100) / 10 },
{ count = 2, path = filepath(dir, "hoge1.txt"), score = 2 * (2 * 100) / 10 },
@ -173,7 +174,7 @@ describe("frecency", function()
describe("benchmark", function()
describe("after registered over >5000 files", function()
with_files({}, function(frecency, dir)
with_files({}, function(frecency, finder, dir)
with_fake_register(frecency, dir, function(register)
-- TODO: 6000 records is too many to use with native?
-- local file_count = 6000
@ -187,10 +188,13 @@ describe("frecency", function()
for i = 1, file_count do
local file = ("hoge%08d.txt"):format(i)
table.insert(expected, { count = 1, path = filepath(dir, file), score = 10 })
-- HACK: disable log because it fails with too many logging
log.new({ level = "info" }, true)
register(file, "2023-07-29T00:00:00+09:00")
log.new({}, true)
end
local start = os.clock()
local results = frecency.picker:fetch_results(nil, "2023-07-29T00:01:00+09:00")
local results = finder:get_results(nil, "2023-07-29T00:01:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
@ -211,13 +215,13 @@ describe("frecency", function()
describe("validate_database", function()
describe("when no files are unlinked", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
it("removes no entries", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00")
assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 },
@ -228,37 +232,9 @@ describe("frecency", function()
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" }, function(frecency, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
register("hoge3.txt", "2023-07-29T00:02:00+09:00")
register("hoge4.txt", "2023-07-29T00:03:00+09:00")
register("hoge5.txt", "2023-07-29T00:04:00+09:00")
frecency.config.db_validate_threshold = 3
dir:joinpath("hoge1.txt"):rm()
dir:joinpath("hoge2.txt"):rm()
frecency:validate_database()
it("removes no entries", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
assert.are.same({
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge3.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 },
}, 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" }, function(frecency, dir)
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
@ -268,52 +244,10 @@ describe("frecency", function()
frecency.config.db_validate_threshold = 3
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 = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
assert.are.same({
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 },
}, results)
end)
end)
end)
describe('when the user response "no"', function()
with_files({ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" }, function(frecency, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
register("hoge3.txt", "2023-07-29T00:02:00+09:00")
register("hoge4.txt", "2023-07-29T00:03:00+09:00")
register("hoge5.txt", "2023-07-29T00:04:00+09:00")
frecency.config.db_validate_threshold = 3
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)
frecency:validate_database()
it("removes no entries", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
@ -325,14 +259,93 @@ describe("frecency", function()
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 },
}, results)
end)
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" },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
register("hoge3.txt", "2023-07-29T00:02:00+09:00")
register("hoge4.txt", "2023-07-29T00:03:00+09:00")
register("hoge5.txt", "2023-07-29T00:04:00+09:00")
frecency.config.db_validate_threshold = 3
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, "2023-07-29T02:00:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
assert.are.same({
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 },
}, results)
end)
end
)
end)
describe('when the user response "no"', function()
with_files(
{ "hoge1.txt", "hoge2.txt", "hoge3.txt", "hoge4.txt", "hoge5.txt" },
function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
register("hoge3.txt", "2023-07-29T00:02:00+09:00")
register("hoge4.txt", "2023-07-29T00:03:00+09:00")
register("hoge5.txt", "2023-07-29T00:04:00+09:00")
frecency.config.db_validate_threshold = 3
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, "2023-07-29T02:00:00+09:00")
table.sort(results, function(a, b)
return a.path < b.path
end)
assert.are.same({
{ count = 1, path = filepath(dir, "hoge1.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge3.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge4.txt"), score = 10 },
{ count = 1, path = filepath(dir, "hoge5.txt"), score = 10 },
}, 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, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
@ -347,7 +360,7 @@ describe("frecency", function()
end)
it("needs confirmation for removing entries", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00")
assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
}, results)
@ -356,7 +369,7 @@ describe("frecency", function()
end)
describe("when db_safe_mode is false", function()
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, dir)
with_files({ "hoge1.txt", "hoge2.txt" }, function(frecency, finder, dir)
local register = make_register(frecency, dir)
register("hoge1.txt", "2023-07-29T00:00:00+09:00")
register("hoge2.txt", "2023-07-29T00:01:00+09:00")
@ -372,7 +385,7 @@ describe("frecency", function()
end)
it("needs no confirmation for removing entries", function()
local results = frecency.picker:fetch_results(nil, "2023-07-29T02:00:00+09:00")
local results = finder:get_results(nil, "2023-07-29T02:00:00+09:00")
assert.are.same({
{ count = 1, path = filepath(dir, "hoge2.txt"), score = 10 },
}, results)

View File

@ -6,6 +6,8 @@ local Sqlite = require "frecency.database.sqlite"
local Native = require "frecency.database.native"
local util = require "frecency.tests.util"
local wait = require "frecency.wait"
-- TODO: replace this with vim.system
local Job = require "plenary.job"
---@param callback fun(migrator: FrecencyMigrator, sqlite: FrecencyDatabase): nil
---@return nil
@ -19,13 +21,27 @@ local function with(callback)
close()
end
local function strptime(iso8601)
local result = vim.fn.strptime("%FT%T%z", iso8601)
return result ~= 0 and result or nil
end
-- NOTE: Windows has no strptime
local function time_piece(iso8601)
local stdout, code =
Job:new({ "perl", "-MTime::Piece", "-e", "print Time::Piece->strptime('" .. iso8601 .. "', '%FT%T%z')->epoch" })
:sync(30000)
return code == 0 and tonumber(stdout[1]) or nil
end
---@param source table<string,{ count: integer, timestamps: string[] }>
local function v1_table(source)
local records = {}
for path, record in pairs(source) do
local timestamps = {}
for _, timestamp in ipairs(record.timestamps) do
table.insert(timestamps, vim.fn.strptime("%FT%T%z", timestamp))
local iso8601 = timestamp .. "+0000"
table.insert(timestamps, strptime(iso8601) or time_piece(iso8601))
end
records[path] = { count = record.count, timestamps = timestamps }
end
@ -44,11 +60,11 @@ describe("migrator", function()
it("has converted into a valid table", function()
assert.are.same(
native.table,
v1_table {
["hoge1.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } },
["hoge2.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } },
},
native.table
["hoge1.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00" } },
["hoge2.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00" } },
}
)
end)
end)
@ -75,13 +91,13 @@ describe("migrator", function()
it("has converted into a valid table", function()
assert.are.same(
native.table,
v1_table {
["hoge1.txt"] = { count = 4, timestamps = { "2023-08-21T00:03:00+0000", "2023-08-21T00:04:00+0000" } },
["hoge2.txt"] = { count = 3, timestamps = { "2023-08-21T00:06:00+0000", "2023-08-21T00:07:00+0000" } },
["hoge3.txt"] = { count = 2, timestamps = { "2023-08-21T00:08:00+0000", "2023-08-21T00:09:00+0000" } },
["hoge4.txt"] = { count = 1, timestamps = { "2023-08-21T00:10:00+0000" } },
},
native.table
["hoge1.txt"] = { count = 4, timestamps = { "2023-08-21T00:03:00", "2023-08-21T00:04:00" } },
["hoge2.txt"] = { count = 3, timestamps = { "2023-08-21T00:06:00", "2023-08-21T00:07:00" } },
["hoge3.txt"] = { count = 2, timestamps = { "2023-08-21T00:08:00", "2023-08-21T00:09:00" } },
["hoge4.txt"] = { count = 1, timestamps = { "2023-08-21T00:10:00" } },
}
)
end)
end)
@ -92,10 +108,10 @@ describe("migrator", function()
with(function(migrator, sqlite)
local native = Native.new(migrator.fs, { root = migrator.root })
native.table = v1_table {
["hoge1.txt"] = { count = 4, timestamps = { "2023-08-21T00:03:00+0000", "2023-08-21T00:04:00+0000" } },
["hoge2.txt"] = { count = 3, timestamps = { "2023-08-21T00:06:00+0000", "2023-08-21T00:07:00+0000" } },
["hoge3.txt"] = { count = 2, timestamps = { "2023-08-21T00:08:00+0000", "2023-08-21T00:09:00+0000" } },
["hoge4.txt"] = { count = 1, timestamps = { "2023-08-21T00:10:00+0000" } },
["hoge1.txt"] = { count = 4, timestamps = { "2023-08-21T00:03:00", "2023-08-21T00:04:00" } },
["hoge2.txt"] = { count = 3, timestamps = { "2023-08-21T00:06:00", "2023-08-21T00:07:00" } },
["hoge3.txt"] = { count = 2, timestamps = { "2023-08-21T00:08:00", "2023-08-21T00:09:00" } },
["hoge4.txt"] = { count = 1, timestamps = { "2023-08-21T00:10:00" } },
}
wait(function()
native:save()
@ -114,7 +130,7 @@ describe("migrator", function()
]]
it("has converted into a valid DB", function()
assert.are.same({
assert.are.same(records, {
{ path = "hoge1.txt", count = 4, datetime = "2023-08-21 00:03:00" },
{ path = "hoge1.txt", count = 4, datetime = "2023-08-21 00:04:00" },
{ path = "hoge2.txt", count = 3, datetime = "2023-08-21 00:06:00" },
@ -122,7 +138,7 @@ describe("migrator", function()
{ path = "hoge3.txt", count = 2, datetime = "2023-08-21 00:08:00" },
{ path = "hoge3.txt", count = 2, datetime = "2023-08-21 00:09:00" },
{ path = "hoge4.txt", count = 1, datetime = "2023-08-21 00:10:00" },
}, records)
})
end)
end)
end)

View File

@ -7,7 +7,6 @@ end
if not vim.env.SQLITE_PATH then
error "set $SQLITE_PATH to find telescope.nvim"
end
vim.opt.runtimepath:append "."
vim.opt.runtimepath:append(vim.env.PLENARY_PATH)
vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH)
vim.opt.runtimepath:append(vim.env.SQLITE_PATH)

View File

@ -0,0 +1,46 @@
local FS = require "frecency.fs"
local Native = require "frecency.database.native"
local async = require "plenary.async" --[[@as PlenaryAsync]]
local util = require "frecency.tests.util"
async.tests.add_to_env()
local function with_native(f)
local fs = FS.new { ignore_patterns = {} }
local dir, close = util.tmpdir()
dir:joinpath("file_frecency.bin"):touch()
return function()
local native = Native.new(fs, { root = dir.filename })
f(native)
close()
end
end
local function save_and_load(native, tbl, datetime)
native:raw_save(util.v1_table(tbl))
async.util.sleep(100)
local entries = native:get_entries(nil, datetime)
table.sort(entries, function(a, b)
return a.path < b.path
end)
return entries
end
a.describe("frecency.database.native", function()
a.describe("updated by another process", function()
a.it(
"returns valid entries",
with_native(function(native)
assert.are.same(
{
{ path = "hoge1.txt", count = 1, ages = { 60 } },
{ path = "hoge2.txt", count = 1, ages = { 60 } },
},
save_and_load(native, {
["hoge1.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } },
["hoge2.txt"] = { count = 1, timestamps = { "2023-08-21T00:00:00+0000" } },
}, "2023-08-21T01:00:00+0000")
)
end)
)
end)
end)

View File

@ -1,5 +1,7 @@
local uv = vim.uv or vim.loop
local async = require "plenary.async" --[[@as PlenaryAsync]]
local Path = require "plenary.path"
local Job = require "plenary.job"
---@return PlenaryPath
---@return fun(): nil close swwp all entries
@ -22,4 +24,35 @@ local function make_tree(entries)
return dir, close
end
return { make_tree = make_tree, tmpdir = tmpdir }
local AsyncJob = async.wrap(function(cmd, callback)
return Job:new({
command = cmd[1],
args = { select(2, unpack(cmd)) },
on_exit = function(self, code, _)
local stdout = code == 0 and table.concat(self:result(), "\n") or nil
callback(stdout, code)
end,
}):start()
end, 2)
-- NOTE: vim.fn.strptime cannot be used in Lua loop
local function time_piece(iso8601)
local stdout, code =
AsyncJob { "perl", "-MTime::Piece", "-e", "print Time::Piece->strptime('" .. iso8601 .. "', '%FT%T%z')->epoch" }
return code == 0 and tonumber(stdout) or nil
end
---@param source table<string,{ count: integer, timestamps: string[] }>
local function v1_table(source)
local records = {}
for path, record in pairs(source) do
local timestamps = {}
for _, iso8601 in ipairs(record.timestamps) do
table.insert(timestamps, time_piece(iso8601))
end
records[path] = { count = record.count, timestamps = timestamps }
end
return { version = "v1", records = records }
end
return { make_tree = make_tree, tmpdir = tmpdir, v1_table = v1_table, time_piece = time_piece }

View File

@ -55,6 +55,7 @@
---@field parent PlenaryPath
---@field path { sep: string }
---@field rm fun(self: PlenaryPath, opts: { recursive: boolean }?): nil
---@field touch fun(self: PlenaryPath, opts: { parents: boolean }?): nil
---@class PlenaryScanDirOptions
---@field hidden boolean if true hidden files will be added
@ -70,9 +71,11 @@
---@class PlenaryAsync
---@field control PlenaryAsyncControl
---@field tests { add_to_env: fun(): nil }
---@field util PlenaryAsyncUtil
---@field uv PlenaryAsyncUv
---@field void fun(f: fun(): nil): fun(): nil
---@field wrap fun(f: (fun(...): any), args: integer): (fun(...): any)
local PlenaryAsync = {}
---@async
@ -85,28 +88,42 @@ function PlenaryAsync.run(f) end
---@class PlenaryAsyncControlChannel
---@field mpsc fun(): PlenaryAsyncControlChannelTx, PlenaryAsyncControlChannelRx
---@field counter fun(): PlenaryAsyncControlChannelTx, PlenaryAsyncControlChannelRx
---@class PlenaryAsyncControlChannelTx
---@field send fun(entry: FrecencyEntry?): nil
---@field send fun(entry: any?): nil
local PlenaryAsyncControlChannelTx = {}
---@class PlenaryAsyncControlChannelRx
local PlenaryAsyncControlChannelRx = {}
---@async
---@return FrecencyEntry?
---@return any?
function PlenaryAsyncControlChannelRx.recv() end
---@async
---@return any?
function PlenaryAsyncControlChannelRx.last() end
---@class PlenaryAsyncUtil
local PlenaryAsyncUtil = {}
---@class PlenaryAsyncUv
local PlenaryAsyncUv = {}
---@class FsStatMtime
---@field sec integer
---@field nsec integer
---@class FsStat
---@field mtime FsStatMtime
---@field size integer
---@field type "file"|"directory"
---@async
---@param path string
---@return string? err
---@return { mtime: integer, size: integer, type: "file"|"directory" }
---@return { mtime: FsStatMtime, size: integer, type: "file"|"directory" }
function PlenaryAsyncUv.fs_stat(path) end
---@async
@ -184,3 +201,8 @@ function PlenaryAsyncUtil.sleep(ms) end
---@class WinInfo
---@field topline integer
---@field botline integer
---@class UvFsEventHandle
---@field stop fun(self: UvFsEventHandle): nil
---@field start fun(self: UvFsEventHandle, path: string, opts: { recursive: boolean }, cb: fun(err: string?, filename: string?, events: string[])): nil
---@field close fun(self: UvFsEventHandle): nil