Skip to content

Commit

Permalink
feat: enable to use multi dirs for each workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
delphinus committed Nov 23, 2024
1 parent 44d1c7b commit 19eded9
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 56 deletions.
4 changes: 2 additions & 2 deletions lua/frecency/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ local os_util = require "frecency.os_util"
---@field show_scores? boolean default: false
---@field show_unindexed? boolean default: true
---@field workspace_scan_cmd? "LUA"|string[] default: nil
---@field workspaces? table<string, string> default: {}
---@field workspaces? table<string, string|string[]> default: {}

---@class FrecencyConfig: FrecencyRawConfig
---@field ext_config FrecencyRawConfig
Expand Down Expand Up @@ -57,7 +57,7 @@ local Config = {}
---@field show_scores boolean default: false
---@field show_unindexed boolean default: true
---@field workspace_scan_cmd? "LUA"|string[] default: nil
---@field workspaces table<string, string> default: {}
---@field workspaces table<string, string|string[]> default: {}

---@return FrecencyConfig
Config.new = function()
Expand Down
19 changes: 16 additions & 3 deletions lua/frecency/database.lua
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,27 @@ function Database:update(path, epoch)
end

---@async
---@param workspace? string
---@param workspaces? string[]
---@param epoch? integer
---@return FrecencyDatabaseEntry[]
function Database:get_entries(workspace, epoch)
function Database:get_entries(workspaces, epoch)
local now = epoch or os.time()
---@param path string
---@return boolean
local function in_workspace(path)
if not workspaces then
return true
end
for _, workspace in ipairs(workspaces) do
if fs.starts_with(path, workspace) then
return true
end
end
return false
end
local items = {}
for path, record in pairs(self.tbl.records) do
if fs.starts_with(path, workspace) then
if in_workspace(path) then
table.insert(items, {
path = path,
count = record.count,
Expand Down
43 changes: 30 additions & 13 deletions lua/frecency/entry_maker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ end
---@alias FrecencyEntryMakerInstance fun(file: FrecencyFile): FrecencyEntry

---@param filepath_formatter FrecencyFilepathFormatter
---@param workspace? string
---@param workspaces? string[]
---@param workspace_tag? string
---@return FrecencyEntryMakerInstance
function EntryMaker:create(filepath_formatter, workspace, workspace_tag)
function EntryMaker:create(filepath_formatter, workspaces, workspace_tag)
-- NOTE: entry_display.create calls non API-fast functions. We cannot call
-- in entry_maker because it will be called in a Lua loop.
local displayer = entry_display.create {
separator = "",
hl_chars = { [Path.path.sep] = "TelescopePathSeparator" },
items = self:width_items(workspace, workspace_tag),
items = self:width_items(workspaces, workspace_tag),
}

-- set loaded buffers for highlight
Expand All @@ -66,18 +66,28 @@ function EntryMaker:create(filepath_formatter, workspace, workspace_tag)
---@param entry FrecencyEntry
---@return table
display = function(entry)
local items = self:items(entry, workspace, workspace_tag, filepath_formatter(workspace))
---@type string
local matched
if workspaces then
for _, workspace in ipairs(workspaces) do
if entry.name:find(workspace, 1, true) then
matched = workspace
break
end
end
end
local items = self:items(entry, matched, workspace_tag, filepath_formatter(matched))
return displayer(items)
end,
}
end
end

---@private
---@param workspace? string
---@param workspaces? string[]
---@param workspace_tag? string
---@return table[]
function EntryMaker:width_items(workspace, workspace_tag)
function EntryMaker:width_items(workspaces, workspace_tag)
local width_items = {}
if config.show_scores then
table.insert(width_items, { width = 5 }) -- recency score
Expand All @@ -89,8 +99,8 @@ function EntryMaker:width_items(workspace, workspace_tag)
if not config.disable_devicons then
table.insert(width_items, { width = 2 })
end
if config.show_filter_column and workspace and workspace_tag then
table.insert(width_items, { width = self:calculate_filter_column_width(workspace, workspace_tag) })
if config.show_filter_column and workspaces and #workspaces > 0 and workspace_tag then
table.insert(width_items, { width = self:calculate_filter_column_width(workspaces, workspace_tag) })
end
-- TODO: This is a stopgap measure to detect placeholders.
table.insert(width_items, {})
Expand Down Expand Up @@ -146,18 +156,25 @@ function EntryMaker:items(entry, workspace, workspace_tag, formatter)
end

---@private
---@param workspace string
---@param workspaces string[]
---@param workspace_tag string
---@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 #(fs.relative_from_home(workspace)) + 1
function EntryMaker:calculate_filter_column_width(workspaces, workspace_tag)
---@type string
local longest
for _, workspace in ipairs(workspaces) do
if not longest or #workspace > #longest then
longest = workspace
end
end
return self:should_show_tail(workspace_tag) and #(utils.path_tail(longest)) + 1
or #(fs.relative_from_home(longest)) + 1
end

---@private
---@param workspace_tag string
---@return boolean
function EntryMaker:should_show_tail(workspace_tag)
function EntryMaker.should_show_tail(_, workspace_tag)
local show_filter_column = config.show_filter_column
local filters = type(show_filter_column) == "table" and show_filter_column or { "LSP", "CWD" }
return vim.tbl_contains(filters, workspace_tag)
Expand Down
20 changes: 10 additions & 10 deletions lua/frecency/finder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@field entries FrecencyEntry[]
---@field scanned_entries FrecencyEntry[]
---@field entry_maker FrecencyEntryMakerInstance
---@field path? string
---@field paths? string[]
---@field private database FrecencyDatabase
---@field private rx FrecencyPlenaryAsyncControlChannelRx
---@field private tx FrecencyPlenaryAsyncControlChannelTx
Expand All @@ -35,26 +35,26 @@ local Finder = {}
---@param database FrecencyDatabase
---@param entry_maker FrecencyEntryMakerInstance
---@param need_scandir boolean
---@param path string?
---@param paths string[]?
---@param state FrecencyState
---@param finder_config? FrecencyFinderConfig
---@return FrecencyFinder
Finder.new = function(database, entry_maker, need_scandir, path, state, finder_config)
Finder.new = function(database, entry_maker, need_scandir, paths, state, finder_config)
local tx, rx = async.control.channel.mpsc()
local scan_tx, scan_rx = async.control.channel.mpsc()
local self = setmetatable({
config = vim.tbl_extend("force", { chunk_size = 1000, sleep_interval = 50 }, finder_config or {}),
closed = false,
database = database,
entry_maker = entry_maker,
path = path,
paths = paths,
state = state,

seen = {},
entries = {},
scanned_entries = {},
need_scan_db = true,
need_scan_dir = need_scandir and path,
need_scan_dir = need_scandir and not not paths,
rx = rx,
tx = tx,
scan_rx = scan_rx,
Expand Down Expand Up @@ -93,7 +93,7 @@ function Finder:start(epoch)
async.void(function()
-- NOTE: return to the main loop to show the main window
async.util.scheduler()
for _, file in ipairs(self:get_results(self.path, epoch)) do
for _, file in ipairs(self:get_results(self.paths, epoch)) do
file.path = os_util.normalize_sep(file.path)
local entry = self.entry_maker(file)
self.tx.send(entry)
Expand Down Expand Up @@ -252,13 +252,13 @@ function Finder:process_channel(process_result, entries, rx, start_index)
end
end

---@param workspace? string
---@param workspaces? string[]
---@param epoch? integer
---@return FrecencyFile[]
function Finder:get_results(workspace, epoch)
log.debug { workspace = workspace or "NONE" }
function Finder:get_results(workspaces, epoch)
log.debug { workspaces = workspaces or "NONE" }
timer.track "fetching start"
local files = self.database:get_entries(workspace, epoch)
local files = self.database:get_entries(workspaces, epoch)
timer.track "fetching finish"
for _, file in ipairs(files) do
file.score = file.ages and recency.calculate(file.count, file.ages) or 0
Expand Down
5 changes: 3 additions & 2 deletions lua/frecency/klass.lua
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ end
---@field limit? integer default: 100
---@field order? FrecencyQueryOrder default: "score"
---@field record? boolean default: false
---@field workspace? string default: nil
---@field workspace? string|string[] default: nil

---@class FrecencyQueryEntry
---@field count integer
Expand All @@ -223,6 +223,7 @@ function Frecency:query(opts, epoch)
order = "score",
record = false,
}, opts or {})
local workspaces=type(opts.workspace)=='table'and opts.workspace or type(opts.workspace)=='string'and {opts.workspace}or nil
---@param entry FrecencyDatabaseEntry
local entries = vim.tbl_map(function(entry)
return {
Expand All @@ -231,7 +232,7 @@ function Frecency:query(opts, epoch)
score = entry.ages and recency.calculate(entry.count, entry.ages) or 0,
timestamps = entry.timestamps,
}
end, self.database:get_entries(opts.workspace, epoch))
end, self.database:get_entries(workspaces, epoch))
table.sort(entries, self:query_sorter(opts.order, opts.direction))
local results = opts.record and entries or vim.tbl_map(function(entry)
return entry.path
Expand Down
76 changes: 50 additions & 26 deletions lua/frecency/picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ local uv = vim.loop or vim.uv
---@field private lsp_workspaces string[]
---@field private namespace integer
---@field private state FrecencyState
---@field private workspace string?
---@field private workspaces string[]?
---@field private workspace_tag_regex string
local Picker = {}

Expand Down Expand Up @@ -64,20 +64,20 @@ end
---| "shorten"
---| "truncate"
---| fun(opts: FrecencyPickerOptions, path: string): string
---@field workspace? string
---@field workspace? string|string[]

---@param opts table
---@param workspace? string
---@param workspaces? string[]
---@param workspace_tag? string
function Picker:finder(opts, workspace, workspace_tag)
function Picker:finder(opts, workspaces, 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 config.show_unindexed)
local entry_maker = self.entry_maker:create(filepath_formatter, workspaces, workspace_tag)
local need_scandir = not not (workspaces and #workspaces > 0 and config.show_unindexed)
return Finder.new(
self.database,
entry_maker,
need_scandir,
workspace,
workspaces,
self.state,
{ ignore_filenames = self.config.ignore_filenames }
)
Expand All @@ -91,11 +91,10 @@ function Picker:start(opts)
return self:default_path_display(picker_opts, path)
end,
}, telescope_config.values, opts or {}) --[[@as FrecencyPickerOptions]]
self.workspace = self:get_workspace(opts.cwd, self.config.initial_workspace_tag or config.default_workspace)
log.debug { workspace = self.workspace }
self.workspaces = self:get_workspaces(opts.cwd, self.config.initial_workspace_tag or config.default_workspace)

self.state = State.new()
local finder = self:finder(opts, self.workspace, self.config.initial_workspace_tag or config.default_workspace)
local finder = self:finder(opts, self.workspaces, self.config.initial_workspace_tag or config.default_workspace)
local picker = pickers.new(opts, {
prompt_title = "Frecency",
finder = finder,
Expand Down Expand Up @@ -140,7 +139,7 @@ end
function Picker:workspace_tags()
local tags = vim.tbl_keys(config.workspaces)
table.insert(tags, "CWD")
if self:get_lsp_workspace() then
if self:get_lsp_workspaces() then
table.insert(tags, "LSP")
end
return tags
Expand All @@ -152,7 +151,7 @@ end
---@return string
function Picker:default_path_display(opts, path)
local filename = Path:new(path):make_relative(opts.cwd)
if not self.workspace then
if not self.workspaces or #self.workspaces == 0 then
if vim.startswith(filename, fs.os_homedir) then
filename = "~" .. Path.path.sep .. fs.relative_from_home(filename)
elseif filename ~= path then
Expand All @@ -165,40 +164,41 @@ end
---@private
---@param cwd string
---@param tag? string
---@return string?
function Picker:get_workspace(cwd, tag)
---@return string[]?
function Picker:get_workspaces(cwd, tag)
if not tag then
return nil
elseif config.workspaces[tag] then
return config.workspaces[tag]
local w = config.workspaces[tag]
return type(w) == "table" and w or { w }
elseif tag == "LSP" then
return self:get_lsp_workspace()
return self:get_lsp_workspaces()
elseif tag == "CWD" then
return cwd
return { cwd }
end
end

---@private
---@return string?
function Picker:get_lsp_workspace()
---@return string[]?
function Picker:get_lsp_workspaces()
if vim.tbl_isempty(self.lsp_workspaces) then
self.lsp_workspaces = vim.api.nvim_buf_call(self.config.editing_bufnr, vim.lsp.buf.list_workspace_folders)
end
return self.lsp_workspaces[1]
return self.lsp_workspaces
end

---@private
---@param picker_opts table
---@return fun(prompt: string): table
function Picker:on_input_filter_cb(picker_opts)
return function(prompt)
local workspace
local workspaces
local start, finish, tag = prompt:find(self.workspace_tag_regex)
local opts = { prompt = start and prompt:sub(finish + 1) or prompt }
if prompt == "" then
workspace = self:get_workspace(picker_opts.cwd, self.config.initial_workspace_tag or config.default_workspace)
workspaces = self:get_workspaces(picker_opts.cwd, self.config.initial_workspace_tag or config.default_workspace)
else
workspace = self:get_workspace(picker_opts.cwd, tag) or self.workspace
workspaces = self:get_workspaces(picker_opts.cwd, tag) or self.workspaces
end
local picker = self.state:get()
if picker then
Expand All @@ -217,10 +217,34 @@ function Picker:on_input_filter_cb(picker_opts)
)
end
end
if self.workspace ~= workspace then
self.workspace = workspace

---@param a? string[]
---@param b? string[]
---@return boolean
local function same_workspaces(a, b)
if not a or not b or #a ~= #b then
return false
end
local function list_to_map(list)
local tmp = {}
for _, v in ipairs(list) do
tmp[v] = true
end
return tmp
end
local a_map, b_map = list_to_map(a), list_to_map(b)
for _, v in ipairs(a_map) do
if not b_map[v] then
return false
end
end
return true
end

if not same_workspaces(self.workspaces, workspaces) then
self.workspaces = workspaces
opts.updated_finder =
self:finder(picker_opts, self.workspace, tag or self.config.initial_workspace_tag or config.default_workspace)
self:finder(picker_opts, self.workspaces, tag or self.config.initial_workspace_tag or config.default_workspace)
opts.updated_finder:start()
end
return opts
Expand Down

0 comments on commit 19eded9

Please sign in to comment.