feat: make query() faster and more lazier (#241)

* refactor: simplify logic to load web_devicons

* refactor: make register() asynchronous

* fix: load lazily modules outside this plugin

* refactor: simplify logic to wait initialization

* refactor: use uv.hrtime() instead of os.clock()

* fix: avoid errors in calling plenary.log in async

* test: store elapsed time to check in tests

* test: fix module names

This becomes a problem only in Ubuntu because macOS and Windows does not
care cases in filenames.

* test: fix types and unused modules

* style: fix by stylua

* refactor: make recency / entry_maker loaded lazily
This commit is contained in:
JINNOUCHI Yasushi 2024-08-25 19:28:52 +09:00 committed by GitHub
parent 673585ee99
commit c140e6ff9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 199 additions and 166 deletions

View File

@ -1,11 +1,13 @@
local Table = require "frecency.database.table" local Table = require "frecency.database.table"
local FileLock = require "frecency.file_lock" local FileLock = require "frecency.file_lock"
local Timer = require "frecency.timer"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs" local fs = require "frecency.fs"
local watcher = require "frecency.watcher" local watcher = require "frecency.watcher"
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local lazy_require = require "frecency.lazy_require"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
---@class FrecencyDatabaseEntry ---@class FrecencyDatabaseEntry
---@field ages number[] ---@field ages number[]
@ -184,7 +186,7 @@ end
---@async ---@async
---@return nil ---@return nil
function Database:load() function Database:load()
local start = os.clock() local timer = Timer.new "load()"
local err, data = self:file_lock():with(function(target) local err, data = self:file_lock():with(function(target)
local err, stat = async.uv.fs_stat(target) local err, stat = async.uv.fs_stat(target)
if err then if err then
@ -203,13 +205,13 @@ function Database:load()
assert(not err, err) assert(not err, err)
local tbl = vim.F.npcall(loadstring(data or "")) local tbl = vim.F.npcall(loadstring(data or ""))
self.tbl:set(tbl) self.tbl:set(tbl)
log.debug(("load() takes %f seconds"):format(os.clock() - start)) timer:finish()
end end
---@async ---@async
---@return nil ---@return nil
function Database:save() function Database:save()
local start = os.clock() local timer = Timer.new "save()"
local err = self:file_lock():with(function(target) local err = self:file_lock():with(function(target)
self:raw_save(self.tbl:raw(), target) self:raw_save(self.tbl:raw(), target)
local err, stat = async.uv.fs_stat(target) local err, stat = async.uv.fs_stat(target)
@ -218,7 +220,7 @@ function Database:save()
return nil return nil
end) end)
assert(not err, err) assert(not err, err)
log.debug(("save() takes %f seconds"):format(os.clock() - start)) timer:finish()
end end
---@async ---@async

View File

@ -1,5 +1,6 @@
local log = require "frecency.log" local Timer = require "frecency.timer"
local async = require "plenary.async" local lazy_require = require "frecency.lazy_require"
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class FrecencyDatabaseRecordValue ---@class FrecencyDatabaseRecordValue
---@field count integer ---@field count integer
@ -47,13 +48,13 @@ end
---@async ---@async
---@return nil ---@return nil
function Table:wait_ready() function Table:wait_ready()
local start = os.clock() local timer = Timer.new "wait_ready()"
local t = 0.2 local t = 0.2
while not rawget(self, "is_ready") do while not rawget(self, "is_ready") do
async.util.sleep(t) async.util.sleep(t)
t = t * 2 t = t * 2
end end
log.debug(("wait_ready() takes %f seconds"):format(os.clock() - start)) timer:finish()
end end
return Table return Table

View File

@ -1,18 +1,18 @@
local WebDevicons = require "frecency.web_devicons" local web_devicons = require "frecency.web_devicons"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs" local fs = require "frecency.fs"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local Path = require "plenary.path"
local entry_display = require "telescope.pickers.entry_display" --[[@as FrecencyTelescopeEntryDisplay]] local lazy_require = require "frecency.lazy_require"
local utils = require "telescope.utils" --[[@as FrecencyTelescopeUtils]] local entry_display = lazy_require "telescope.pickers.entry_display" --[[@as FrecencyTelescopeEntryDisplay]]
local utils = lazy_require "telescope.utils" --[[@as FrecencyTelescopeUtils]]
---@class FrecencyEntryMaker ---@class FrecencyEntryMaker
---@field loaded table<string,boolean> ---@field loaded table<string,boolean>
---@field web_devicons WebDevicons
local EntryMaker = {} local EntryMaker = {}
---@return FrecencyEntryMaker ---@return FrecencyEntryMaker
EntryMaker.new = function() EntryMaker.new = function()
return setmetatable({ web_devicons = WebDevicons.new() }, { __index = EntryMaker }) return setmetatable({}, { __index = EntryMaker })
end end
---@class FrecencyEntry ---@class FrecencyEntry
@ -86,7 +86,7 @@ function EntryMaker:width_items(workspace, workspace_tag)
table.insert(width_items, { width = 6 }) -- fuzzy score table.insert(width_items, { width = 6 }) -- fuzzy score
end end
end end
if self.web_devicons.is_enabled then if not config.disable_devicons then
table.insert(width_items, { width = 2 }) table.insert(width_items, { width = 2 })
end end
if config.show_filter_column and workspace and workspace_tag then if config.show_filter_column and workspace and workspace_tag then
@ -116,12 +116,11 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
table.insert(items, { score, "TelescopeFrecencyScores" }) table.insert(items, { score, "TelescopeFrecencyScores" })
end end
end end
if self.web_devicons.is_enabled then if not config.disable_devicons then
local basename = utils.path_tail(entry.name) local basename = utils.path_tail(entry.name)
local icon, icon_highlight = local icon, icon_highlight = web_devicons.get_icon(basename, utils.file_extension(basename), { default = false })
self.web_devicons:get_icon(basename, utils.file_extension(basename), { default = false })
if not icon then if not icon then
icon, icon_highlight = self.web_devicons:get_icon(basename, nil, { default = true }) icon, icon_highlight = web_devicons.get_icon(basename, nil, { default = true })
end end
table.insert(items, { icon, icon_highlight }) table.insert(items, { icon, icon_highlight })
end end

View File

@ -1,6 +1,7 @@
local log = require "frecency.log" local log = require "frecency.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local lazy_require = require "frecency.lazy_require"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class FrecencyFileLock ---@class FrecencyFileLock
---@field base string ---@field base string

View File

@ -1,9 +1,12 @@
local Timer = require "frecency.timer"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs" local fs = require "frecency.fs"
local os_util = require "frecency.os_util" local os_util = require "frecency.os_util"
local recency = require "frecency.recency"
local log = require "frecency.log" local log = require "frecency.log"
local Job = require "plenary.job" local lazy_require = require "frecency.lazy_require"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local Job = lazy_require "plenary.job" --[[@as FrecencyPlenaryJob]]
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class FrecencyFinder ---@class FrecencyFinder
---@field config FrecencyFinderConfig ---@field config FrecencyFinderConfig
@ -21,7 +24,6 @@ local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@field private need_scan_dir boolean ---@field private need_scan_dir boolean
---@field private seen table<string, boolean> ---@field private seen table<string, boolean>
---@field private process VimSystemObj? ---@field private process VimSystemObj?
---@field private recency FrecencyRecency
---@field private state FrecencyState ---@field private state FrecencyState
local Finder = {} local Finder = {}
@ -34,11 +36,10 @@ local Finder = {}
---@param entry_maker FrecencyEntryMakerInstance ---@param entry_maker FrecencyEntryMakerInstance
---@param need_scandir boolean ---@param need_scandir boolean
---@param path string? ---@param path string?
---@param recency FrecencyRecency
---@param state FrecencyState ---@param state FrecencyState
---@param finder_config? FrecencyFinderConfig ---@param finder_config? FrecencyFinderConfig
---@return FrecencyFinder ---@return FrecencyFinder
Finder.new = function(database, entry_maker, need_scandir, path, recency, state, finder_config) Finder.new = function(database, entry_maker, need_scandir, path, state, finder_config)
local tx, rx = async.control.channel.mpsc() local tx, rx = async.control.channel.mpsc()
local scan_tx, scan_rx = async.control.channel.mpsc() local scan_tx, scan_rx = async.control.channel.mpsc()
local self = setmetatable({ local self = setmetatable({
@ -47,7 +48,6 @@ Finder.new = function(database, entry_maker, need_scandir, path, recency, state,
database = database, database = database,
entry_maker = entry_maker, entry_maker = entry_maker,
path = path, path = path,
recency = recency,
state = state, state = state,
seen = {}, seen = {},
@ -257,25 +257,21 @@ end
---@return FrecencyFile[] ---@return FrecencyFile[]
function Finder:get_results(workspace, epoch) function Finder:get_results(workspace, epoch)
log.debug { workspace = workspace or "NONE" } log.debug { workspace = workspace or "NONE" }
local start_fetch = os.clock() local timer_fetch = Timer.new "fetching entries"
local files = self.database:get_entries(workspace, epoch) local files = self.database:get_entries(workspace, epoch)
log.debug(("it takes %f seconds in fetching entries"):format(os.clock() - start_fetch)) timer_fetch:finish()
local start_results = os.clock() local timer_results = Timer.new "making results"
local elapsed_recency = 0
for _, file in ipairs(files) do for _, file in ipairs(files) do
local start_recency = os.clock() file.score = file.ages and recency.calculate(file.count, file.ages) or 0
file.score = file.ages and self.recency:calculate(file.count, file.ages) or 0
file.ages = nil file.ages = nil
elapsed_recency = elapsed_recency + (os.clock() - start_recency)
end end
log.debug(("it takes %f seconds in calculating recency"):format(elapsed_recency)) timer_results:finish()
log.debug(("it takes %f seconds in making results"):format(os.clock() - start_results))
local start_sort = os.clock() local timer_sort = Timer.new "sorting"
table.sort(files, function(a, b) table.sort(files, function(a, b)
return a.score > b.score or (a.score == b.score and a.path > b.path) return a.score > b.score or (a.score == b.score and a.path > b.path)
end) end)
log.debug(("it takes %f seconds in sorting"):format(os.clock() - start_sort)) timer_sort:finish()
return files return files
end end

View File

@ -1,11 +1,13 @@
local config = require "frecency.config" local config = require "frecency.config"
local os_util = require "frecency.os_util" local os_util = require "frecency.os_util"
local log = require "frecency.log" local log = require "frecency.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local lazy_require = require "frecency.lazy_require"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
local scandir = require "plenary.scandir" local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local scandir = lazy_require "plenary.scandir"
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
---@class FrecencyFS
local M = { local M = {
os_homedir = assert(uv.os_homedir()), os_homedir = assert(uv.os_homedir()),
} }

View File

@ -1,5 +1,6 @@
local config = require "frecency.config" local config = require "frecency.config"
local sorters = require "telescope.sorters" local lazy_require = require "frecency.lazy_require"
local sorters = lazy_require "telescope.sorters"
---@param opts any options for get_fzy_sorter() ---@param opts any options for get_fzy_sorter()
return function(opts) return function(opts)

View File

@ -5,7 +5,7 @@
---@field complete fun(findstart: 1|0, base: string): integer|''|string[] ---@field complete fun(findstart: 1|0, base: string): integer|''|string[]
---@field delete async fun(path: string): nil ---@field delete async fun(path: string): nil
---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[] ---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[]
---@field register fun(bufnr: integer, datetime: string?): nil ---@field register async fun(bufnr: integer, datetime: string?): nil
---@field start fun(opts: FrecencyPickerOptions?): nil ---@field start fun(opts: FrecencyPickerOptions?): nil
---@field validate_database async fun(force: boolean?): nil ---@field validate_database async fun(force: boolean?): nil
local frecency = setmetatable({}, { local frecency = setmetatable({}, {
@ -43,7 +43,9 @@ local function setup(ext_config)
return return
end end
require("frecency.config").setup(ext_config) local config = require "frecency.config"
config.setup(ext_config)
vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true }) vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true })
vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true }) vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true })
@ -76,9 +78,10 @@ local function setup(ext_config)
return return
end end
local is_floatwin = vim.api.nvim_win_get_config(0).relative ~= "" local is_floatwin = vim.api.nvim_win_get_config(0).relative ~= ""
if not is_floatwin then if is_floatwin or (config.ignore_register and config.ignore_register(args.buf)) then
frecency.register(args.buf) return
end end
async_call(frecency.register, args.buf, vim.api.nvim_buf_get_name(args.buf))
end, end,
}) })

View File

@ -1,54 +1,46 @@
local Database = require "frecency.database" local Database = require "frecency.database"
local EntryMaker = require "frecency.entry_maker"
local Picker = require "frecency.picker" local Picker = require "frecency.picker"
local Recency = require "frecency.recency" local Timer = require "frecency.timer"
local config = require "frecency.config" local config = require "frecency.config"
local fs = require "frecency.fs" local fs = require "frecency.fs"
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local recency = require "frecency.recency"
local wait = require "frecency.wait"
local lazy_require = require "frecency.lazy_require"
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@class Frecency ---@class Frecency
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database. ---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private entry_maker FrecencyEntryMaker
---@field private picker FrecencyPicker ---@field private picker FrecencyPicker
---@field private recency FrecencyRecency
local Frecency = {} local Frecency = {}
---@return Frecency ---@return Frecency
Frecency.new = function() Frecency.new = function()
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]] local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]]
self.database = Database.new() self.database = Database.new()
self.entry_maker = EntryMaker.new()
self.recency = Recency.new()
return self return self
end end
---This is called when `:Telescope frecency` is called at the first time. ---This is called when `:Telescope frecency` is called at the first time.
---@return nil ---@return nil
function Frecency:setup() function Frecency:setup()
local done = false
---@async ---@async
local function init() local function init()
local timer = Timer.new "init()"
self.database:start() self.database:start()
self:assert_db_entries() self:assert_db_entries()
if config.auto_validate then if config.auto_validate then
self:validate_database() self:validate_database()
end end
done = true timer:finish()
end end
local is_async = not not coroutine.running() local is_async = not not coroutine.running()
if is_async then if is_async then
init() init()
else else
async.void(init)() wait(init)
local ok, status = vim.wait(10000, function()
return done
end)
if not ok then
error("failed to setup:" .. (status == -1 and "timed out" or "interrupted"))
end
end end
end end
@ -56,7 +48,7 @@ end
---@param opts? FrecencyPickerOptions ---@param opts? FrecencyPickerOptions
---@return nil ---@return nil
function Frecency:start(opts) function Frecency:start(opts)
local start = os.clock() local timer = Timer.new "start()"
log.debug "Frecency:start" log.debug "Frecency:start"
opts = opts or {} opts = opts or {}
if opts.cwd then if opts.cwd then
@ -66,13 +58,13 @@ function Frecency:start(opts)
if opts.hide_current_buffer or config.hide_current_buffer then if opts.hide_current_buffer or config.hide_current_buffer then
ignore_filenames = { vim.api.nvim_buf_get_name(0) } ignore_filenames = { vim.api.nvim_buf_get_name(0) }
end end
self.picker = Picker.new(self.database, self.entry_maker, self.recency, { self.picker = Picker.new(self.database, {
editing_bufnr = vim.api.nvim_get_current_buf(), editing_bufnr = vim.api.nvim_get_current_buf(),
ignore_filenames = ignore_filenames, ignore_filenames = ignore_filenames,
initial_workspace_tag = opts.workspace, initial_workspace_tag = opts.workspace,
}) })
self.picker:start(vim.tbl_extend("force", config.get(), opts)) self.picker:start(vim.tbl_extend("force", config.get(), opts))
log.debug(("Frecency:start picker:start takes %f seconds"):format(os.clock() - start)) timer:finish()
end end
---This can be calledBy `require("telescope").extensions.frecency.complete`. ---This can be calledBy `require("telescope").extensions.frecency.complete`.
@ -128,15 +120,12 @@ function Frecency:assert_db_entries()
end end
end end
---@async
---@param bufnr integer ---@param bufnr integer
---@param path string
---@param epoch? integer ---@param epoch? integer
function Frecency:register(bufnr, epoch) function Frecency:register(bufnr, path, epoch)
if (config.ignore_register and config.ignore_register(bufnr)) or self.buf_registered[bufnr] then if self.buf_registered[bufnr] or not fs.is_valid_path(path) then
return
end
local path = vim.api.nvim_buf_get_name(bufnr)
async.void(function()
if not fs.is_valid_path(path) then
return return
end end
local err, realpath = async.uv.fs_realpath(path) local err, realpath = async.uv.fs_realpath(path)
@ -146,7 +135,6 @@ function Frecency:register(bufnr, epoch)
self.database:update(realpath, epoch) self.database:update(realpath, epoch)
self.buf_registered[bufnr] = true self.buf_registered[bufnr] = true
log.debug("registered:", bufnr, path) log.debug("registered:", bufnr, path)
end)()
end end
---@async ---@async
@ -191,7 +179,7 @@ function Frecency:query(opts, epoch)
return { return {
count = entry.count, count = entry.count,
path = entry.path, path = entry.path,
score = entry.ages and self.recency:calculate(entry.count, entry.ages) or 0, score = entry.ages and recency.calculate(entry.count, entry.ages) or 0,
timestamps = entry.timestamps, timestamps = entry.timestamps,
} }
end, self.database:get_entries(opts.workspace, epoch)) end, self.database:get_entries(opts.workspace, epoch))

View File

@ -0,0 +1,11 @@
---@param module string
return function(module)
return setmetatable({}, {
__index = function(_, key)
return require(module)[key]
end,
__call = function(_, ...)
return require(module)(...)
end,
})
end

View File

@ -1,8 +1,11 @@
local config = require "frecency.config" local config = require "frecency.config"
local log = require "plenary.log" local lazy_require = require "frecency.lazy_require"
local log = lazy_require "plenary.log"
return setmetatable({}, { return setmetatable({}, {
__index = function(_, key) __index = function(_, key)
return config.debug and log[key] or function() end return config.debug and vim.schedule_wrap(function(...)
log[key](...)
end) or function() end
end, end,
}) })

View File

@ -1,6 +1,8 @@
local Path = require "plenary.path" local lazy_require = require "frecency.lazy_require"
local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
---@class FrecencyOSUtil
local M = { local M = {
is_windows = uv.os_uname().sysname == "Windows_NT", is_windows = uv.os_uname().sysname == "Windows_NT",
} }

View File

@ -1,3 +1,4 @@
local EntryMaker = require "frecency.entry_maker"
local State = require "frecency.state" local State = require "frecency.state"
local Finder = require "frecency.finder" local Finder = require "frecency.finder"
local config = require "frecency.config" local config = require "frecency.config"
@ -5,11 +6,12 @@ local fs = require "frecency.fs"
local fuzzy_sorter = require "frecency.fuzzy_sorter" local fuzzy_sorter = require "frecency.fuzzy_sorter"
local substr_sorter = require "frecency.substr_sorter" local substr_sorter = require "frecency.substr_sorter"
local log = require "frecency.log" local log = require "frecency.log"
local Path = require "plenary.path" --[[@as FrecencyPlenaryPath]] local lazy_require = require "frecency.lazy_require"
local actions = require "telescope.actions" local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
local config_values = require("telescope.config").values local actions = lazy_require "telescope.actions"
local pickers = require "telescope.pickers" local telescope_config = lazy_require "telescope.config"
local utils = require "telescope.utils" --[[@as FrecencyTelescopeUtils]] local pickers = lazy_require "telescope.pickers"
local utils = lazy_require "telescope.utils" --[[@as FrecencyTelescopeUtils]]
local uv = vim.loop or vim.uv local uv = vim.loop or vim.uv
---@class FrecencyPicker ---@class FrecencyPicker
@ -18,7 +20,6 @@ local uv = vim.loop or vim.uv
---@field private entry_maker FrecencyEntryMaker ---@field private entry_maker FrecencyEntryMaker
---@field private lsp_workspaces string[] ---@field private lsp_workspaces string[]
---@field private namespace integer ---@field private namespace integer
---@field private recency FrecencyRecency
---@field private state FrecencyState ---@field private state FrecencyState
---@field private workspace string? ---@field private workspace string?
---@field private workspace_tag_regex string ---@field private workspace_tag_regex string
@ -37,18 +38,15 @@ local Picker = {}
---@field score number ---@field score number
---@param database FrecencyDatabase ---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMaker
---@param recency FrecencyRecency
---@param picker_config FrecencyPickerConfig ---@param picker_config FrecencyPickerConfig
---@return FrecencyPicker ---@return FrecencyPicker
Picker.new = function(database, entry_maker, recency, picker_config) Picker.new = function(database, picker_config)
local self = setmetatable({ local self = setmetatable({
config = picker_config, config = picker_config,
database = database, database = database,
entry_maker = entry_maker, entry_maker = EntryMaker.new(),
lsp_workspaces = {}, lsp_workspaces = {},
namespace = vim.api.nvim_create_namespace "frecency", namespace = vim.api.nvim_create_namespace "frecency",
recency = recency,
}, { __index = Picker }) }, { __index = Picker })
local d = config.filter_delimiter local d = config.filter_delimiter
self.workspace_tag_regex = "^%s*" .. d .. "(%S+)" .. d self.workspace_tag_regex = "^%s*" .. d .. "(%S+)" .. d
@ -80,7 +78,6 @@ function Picker:finder(opts, workspace, workspace_tag)
entry_maker, entry_maker,
need_scandir, need_scandir,
workspace, workspace,
self.recency,
self.state, self.state,
{ ignore_filenames = self.config.ignore_filenames } { ignore_filenames = self.config.ignore_filenames }
) )
@ -93,7 +90,7 @@ function Picker:start(opts)
path_display = function(picker_opts, path) path_display = function(picker_opts, path)
return self:default_path_display(picker_opts, path) return self:default_path_display(picker_opts, path)
end, end,
}, config_values, opts or {}) --[[@as FrecencyPickerOptions]] }, telescope_config.values, opts or {}) --[[@as FrecencyPickerOptions]]
self.workspace = self:get_workspace(opts.cwd, self.config.initial_workspace_tag or config.default_workspace) self.workspace = self:get_workspace(opts.cwd, self.config.initial_workspace_tag or config.default_workspace)
log.debug { workspace = self.workspace } log.debug { workspace = self.workspace }
@ -102,7 +99,7 @@ function Picker:start(opts)
local picker = pickers.new(opts, { local picker = pickers.new(opts, {
prompt_title = "Frecency", prompt_title = "Frecency",
finder = finder, finder = finder,
previewer = config_values.file_previewer(opts), previewer = telescope_config.values.file_previewer(opts),
sorter = config.matcher == "default" and substr_sorter() or fuzzy_sorter(opts), sorter = config.matcher == "default" and substr_sorter() or fuzzy_sorter(opts),
on_input_filter_cb = self:on_input_filter_cb(opts), on_input_filter_cb = self:on_input_filter_cb(opts),
attach_mappings = function(prompt_bufnr) attach_mappings = function(prompt_bufnr)

View File

@ -1,23 +1,15 @@
local config = require "frecency.config" local config = require "frecency.config"
---@class FrecencyRecency ---@class FrecencyRecency
---@field private modifier table<integer, { age: integer, value: integer }> local M = {}
local Recency = {}
---@return FrecencyRecency
Recency.new = function()
return setmetatable({
modifier = config.recency_values,
}, { __index = Recency })
end
---@param count integer ---@param count integer
---@param ages number[] ---@param ages number[]
---@return number ---@return number
function Recency:calculate(count, ages) function M.calculate(count, ages)
local score = 0 local score = 0
for _, age in ipairs(ages) do for _, age in ipairs(ages) do
for _, rank in ipairs(self.modifier) do for _, rank in ipairs(config.recency_values) do
if age <= rank.age then if age <= rank.age then
score = score + rank.value score = score + rank.value
goto continue goto continue
@ -28,4 +20,4 @@ function Recency:calculate(count, ages)
return count * score / config.max_timestamps return count * score / config.max_timestamps
end end
return Recency return M

View File

@ -1,8 +1,9 @@
-- TODO: use this module until telescope's release include this below. -- TODO: use this module until telescope's release include this below.
-- https://github.com/nvim-telescope/telescope.nvim/pull/2950 -- https://github.com/nvim-telescope/telescope.nvim/pull/2950
local sorters = require "telescope.sorters" local lazy_require = require "frecency.lazy_require"
local util = require "telescope.utils" local sorters = lazy_require "telescope.sorters"
local util = lazy_require "telescope.utils"
local substr_highlighter = function(make_display) local substr_highlighter = function(make_display)
return function(_, prompt, display) return function(_, prompt, display)

View File

@ -2,6 +2,7 @@
-- https://github.com/nvim-lua/plenary.nvim/blob/663246936325062427597964d81d30eaa42ab1e4/lua/plenary/test_harness.lua#L86-L86 -- https://github.com/nvim-lua/plenary.nvim/blob/663246936325062427597964d81d30eaa42ab1e4/lua/plenary/test_harness.lua#L86-L86
vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH) vim.opt.runtimepath:append(vim.env.TELESCOPE_PATH)
local Timer = require "frecency.timer"
local util = require "frecency.tests.util" local util = require "frecency.tests.util"
local log = require "plenary.log" local log = require "plenary.log"
@ -183,7 +184,7 @@ describe("frecency", function()
register(file, make_epoch "2023-07-29T00:00:00+09:00") register(file, make_epoch "2023-07-29T00:00:00+09:00")
log.new({}, true) log.new({}, true)
end end
local start = os.clock() local timer = Timer.new "all results"
local results = vim.tbl_map(function(result) local results = vim.tbl_map(function(result)
result.timestamps = nil result.timestamps = nil
return result return result
@ -191,11 +192,10 @@ describe("frecency", function()
table.sort(results, function(a, b) table.sort(results, function(a, b)
return a.path < b.path return a.path < b.path
end) end)
local elapsed = os.clock() - start timer:finish()
log.info(("it takes %f seconds in fetching all results"):format(elapsed))
it("returns appropriate latency (<1.0 second)", function() it("returns appropriate latency (<1.0 second)", function()
assert.are.is_true(elapsed < 1.0) assert.are.is_true(timer.elapsed < 1.0)
end) end)
it("returns valid response", function() it("returns valid response", function()

View File

@ -1,5 +1,4 @@
local Recency = require "frecency.recency" local recency = require "frecency.recency"
local recency = Recency.new()
describe("frecency.recency", function() describe("frecency.recency", function()
for _, c in ipairs { for _, c in ipairs {
@ -13,7 +12,7 @@ describe("frecency.recency", function()
} do } do
local dumped = vim.inspect(c.ages, { indent = "", newline = "" }) local dumped = vim.inspect(c.ages, { indent = "", newline = "" })
it(("%d, %s => %d"):format(c.count, dumped, c.score), function() it(("%d, %s => %d"):format(c.count, dumped, c.score), function()
assert.are.same(c.score, recency:calculate(c.count, c.ages)) assert.are.same(c.score, recency.calculate(c.count, c.ages))
end) end)
end end
end) end)

View File

@ -7,7 +7,7 @@ local log = require "plenary.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local Path = require "plenary.path" local Path = require "plenary.path"
local Job = require "plenary.job" local Job = require "plenary.job"
local wait = require "frecency.tests.wait" local wait = require "frecency.wait"
---@return FrecencyPlenaryPath ---@return FrecencyPlenaryPath
---@return fun(): nil close swwp all entries ---@return fun(): nil close swwp all entries
@ -101,7 +101,7 @@ local function with_files(files, cb_or_config, callback)
frecency.database:start() frecency.database:start()
frecency.database.tbl:wait_ready() frecency.database.tbl:wait_ready()
end) end)
frecency.picker = Picker.new(frecency.database, frecency.entry_maker, frecency.recency, { editing_bufnr = 0 }) frecency.picker = Picker.new(frecency.database, { editing_bufnr = 0 })
local finder = frecency.picker:finder {} local finder = frecency.picker:finder {}
callback(frecency, finder, dir) callback(frecency, finder, dir)
close() close()
@ -116,16 +116,28 @@ end
---@return fun(file: string, epoch: integer, reset: boolean?, wipeout?: boolean): nil reset: boolean?): nil ---@return fun(file: string, epoch: integer, reset: boolean?, wipeout?: boolean): nil reset: boolean?): nil
local function make_register(frecency, dir) local function make_register(frecency, dir)
return function(file, epoch, reset, wipeout) return function(file, epoch, reset, wipeout)
-- NOTE: this function does the same thing as BufWinEnter autocmd.
---@param bufnr integer
local function register(bufnr)
if vim.api.nvim_buf_get_name(bufnr) == "" then
return
end
local is_floatwin = vim.api.nvim_win_get_config(0).relative ~= ""
if is_floatwin or (config.ignore_register and config.ignore_register(bufnr)) then
return
end
async.util.block_on(function()
frecency:register(bufnr, vim.api.nvim_buf_get_name(bufnr), epoch)
end)
end
local path = filepath(dir, file) local path = filepath(dir, file)
vim.cmd.edit(path) vim.cmd.edit(path)
local bufnr = assert(vim.fn.bufnr(path)) local bufnr = assert(vim.fn.bufnr(path))
if reset then if reset then
frecency.buf_registered[bufnr] = nil frecency.buf_registered[bufnr] = nil
end end
frecency:register(bufnr, epoch) register(bufnr)
vim.wait(1000, function()
return not not frecency.buf_registered[bufnr]
end)
-- HACK: This is needed because almost the same filenames use the same -- HACK: This is needed because almost the same filenames use the same
-- buffer. -- buffer.
if wipeout then if wipeout then
@ -154,7 +166,7 @@ local function with_fake_register(frecency, dir, callback)
bufnr = bufnr + 1 bufnr = bufnr + 1
buffers[bufnr] = path buffers[bufnr] = path
async.util.block_on(function() async.util.block_on(function()
frecency:register(bufnr, epoch) frecency:register(bufnr, path, epoch)
end) end)
end end
callback(register) callback(register)

26
lua/frecency/timer.lua Normal file
View File

@ -0,0 +1,26 @@
local config = require "frecency.config"
local log = require "frecency.log"
local uv = vim.uv or vim.loop
---@class FrecencyTimer
---@field elapsed number
---@field start integer
---@field title string
local Timer = {}
---@param title string
---@return FrecencyTimer
Timer.new = function(title)
return setmetatable({ start = uv.hrtime(), title = title }, { __index = Timer })
end
---@return nil
function Timer:finish()
if not config.debug then
return
end
self.elapsed = (uv.hrtime() - self.start) / 1000000000
log.debug(("[%s] takes %.3f seconds"):format(self.title, self.elapsed))
end
return Timer

View File

@ -2,6 +2,20 @@
-- NOTE: types are borrowed from plenary.nvim -- NOTE: types are borrowed from plenary.nvim
---@class FrecencyPlenaryJob
---@field new fun(self: FrecencyPlenaryJob, opts: FrecencyPlenaryJobOpts): FrecencyPlenaryJob
---@field start fun(self: FrecencyPlenaryJob): nil
---@field handle VimSystemObj uv_process_t
---@class FrecencyPlenaryJobOpts
---@field cwd? string
---@field command? string
---@field args? string[]
---@field on_stdout? FrecencyPlenaryJobCallback
---@field on_stderr? FrecencyPlenaryJobCallback
---@alias FrecencyPlenaryJobCallback fun(error: string, data: string, self?: FrecencyPlenaryJob)
---@class FrecencyPlenaryPath ---@class FrecencyPlenaryPath
---@field new fun(self: FrecencyPlenaryPath|string, path?: string): FrecencyPlenaryPath ---@field new fun(self: FrecencyPlenaryPath|string, path?: string): FrecencyPlenaryPath
---@field absolute fun(): string ---@field absolute fun(): string

View File

@ -1,19 +1,20 @@
local log = require "frecency.log" local log = require "frecency.log"
local async = require "plenary.async" --[[@as FrecencyPlenaryAsync]] local lazy_require = require "frecency.lazy_require"
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
local uv = vim.loop or vim.uv local uv = vim.loop or vim.uv
---@class FrecencyNativeWatcherMtime ---@class FrecencyWatcherMtime
---@field sec integer ---@field sec integer
---@field nsec integer ---@field nsec integer
local Mtime = {} local Mtime = {}
---@param mtime FsStatMtime ---@param mtime FsStatMtime
---@return FrecencyNativeWatcherMtime ---@return FrecencyWatcherMtime
Mtime.new = function(mtime) Mtime.new = function(mtime)
return setmetatable({ sec = mtime.sec, nsec = mtime.nsec }, Mtime) return setmetatable({ sec = mtime.sec, nsec = mtime.nsec }, Mtime)
end end
---@param other FrecencyNativeWatcherMtime ---@param other FrecencyWatcherMtime
---@return boolean ---@return boolean
function Mtime:__eq(other) function Mtime:__eq(other)
return self.sec == other.sec and self.nsec == other.nsec return self.sec == other.sec and self.nsec == other.nsec
@ -24,13 +25,13 @@ function Mtime:__tostring()
return string.format("%d.%d", self.sec, self.nsec) return string.format("%d.%d", self.sec, self.nsec)
end end
---@class FrecencyNativeWatcher ---@class FrecencyWatcher
---@field handler UvFsEventHandle ---@field handler UvFsEventHandle
---@field path string ---@field path string
---@field mtime FrecencyNativeWatcherMtime ---@field mtime FrecencyWatcherMtime
local Watcher = {} local Watcher = {}
---@return FrecencyNativeWatcher ---@return FrecencyWatcher
Watcher.new = function() Watcher.new = function()
return setmetatable({ path = "", mtime = Mtime.new { sec = 0, nsec = 0 } }, { __index = Watcher }) return setmetatable({ path = "", mtime = Mtime.new { sec = 0, nsec = 0 } }, { __index = Watcher })
end end

View File

@ -1,32 +1,14 @@
local config = require "frecency.config" ---@class FrecencyWebDevicons
local M = {
---@class WebDeviconsModule
---@field get_icon fun(name?: string, ext?: string, opts?: table): string, string
---@class WebDevicons
---@field is_enabled boolean
---@field private web_devicons WebDeviconsModule
local WebDevicons = {}
---@return WebDevicons
WebDevicons.new = function()
local ok, web_devicons = pcall(require, "nvim-web-devicons")
return setmetatable(
{ is_enabled = not config.disable_devicons and ok, web_devicons = web_devicons },
{ __index = WebDevicons }
)
end
---@param name string? ---@param name string?
---@param ext string? ---@param ext string?
---@param opts table? ---@param opts table?
---@return string ---@return string
---@return string ---@return string
function WebDevicons:get_icon(name, ext, opts) get_icon = function(name, ext, opts)
if self.is_enabled then local ok, web_devicons = pcall(require, "nvim-web-devicons")
return self.web_devicons.get_icon(name, ext, opts) return ok and web_devicons.get_icon(name, ext, opts) or "", ""
end end,
return "", "" }
end
return WebDevicons return M