diff --git a/lua/frecency/config.lua b/lua/frecency/config.lua index 6f7097e..93d1ef8 100644 --- a/lua/frecency/config.lua +++ b/lua/frecency/config.lua @@ -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 default: {} +---@field workspaces? table default: {} ---@class FrecencyConfig: FrecencyRawConfig ---@field ext_config FrecencyRawConfig @@ -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 default: {} +---@field workspaces table default: {} ---@return FrecencyConfig Config.new = function() diff --git a/lua/frecency/database.lua b/lua/frecency/database.lua index 233d523..1c1110b 100644 --- a/lua/frecency/database.lua +++ b/lua/frecency/database.lua @@ -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, diff --git a/lua/frecency/entry_maker.lua b/lua/frecency/entry_maker.lua index 17bea10..5975c53 100644 --- a/lua/frecency/entry_maker.lua +++ b/lua/frecency/entry_maker.lua @@ -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 @@ -66,7 +66,17 @@ 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, } @@ -74,10 +84,10 @@ function EntryMaker:create(filepath_formatter, workspace, workspace_tag) 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 @@ -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, {}) @@ -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) diff --git a/lua/frecency/finder.lua b/lua/frecency/finder.lua index e405e10..cb9525c 100644 --- a/lua/frecency/finder.lua +++ b/lua/frecency/finder.lua @@ -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 @@ -35,11 +35,11 @@ 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({ @@ -47,14 +47,14 @@ Finder.new = function(database, entry_maker, need_scandir, path, state, finder_c 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, @@ -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) @@ -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 diff --git a/lua/frecency/klass.lua b/lua/frecency/klass.lua index 0635c72..332231e 100644 --- a/lua/frecency/klass.lua +++ b/lua/frecency/klass.lua @@ -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 @@ -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 { @@ -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 diff --git a/lua/frecency/picker.lua b/lua/frecency/picker.lua index 5a0196d..0a2be4f 100644 --- a/lua/frecency/picker.lua +++ b/lua/frecency/picker.lua @@ -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 = {} @@ -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 } ) @@ -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, @@ -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 @@ -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 @@ -165,26 +164,27 @@ 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 @@ -192,13 +192,13 @@ end ---@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 @@ -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