From 7501f1e2c2486fa16a65827e617220649a08fd49 Mon Sep 17 00:00:00 2001 From: delphinus Date: Mon, 9 Sep 2024 01:13:46 +0900 Subject: [PATCH] feat: enable to use multi dirs for each workspace --- lua/frecency/config.lua | 4 +- lua/frecency/database.lua | 19 ++++- lua/frecency/entry_maker.lua | 42 +++++++---- lua/frecency/finder.lua | 134 ++++++++++++++++++++++++----------- lua/frecency/klass.lua | 5 +- lua/frecency/picker.lua | 75 +++++++++++++------- 6 files changed, 190 insertions(+), 89 deletions(-) diff --git a/lua/frecency/config.lua b/lua/frecency/config.lua index 6f7097e8..93d1ef89 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 233d5237..1c1110b7 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 17bea107..5c16ba6b 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,24 @@ 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) + 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 e405e107..48282714 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 @@ -23,9 +23,40 @@ local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]] ---@field private need_scan_db boolean ---@field private need_scan_dir boolean ---@field private seen table ----@field private process VimSystemObj? +---@field private process table ---@field private state FrecencyState -local Finder = {} +local Finder = { + ---@type fun(): string[]? + cmd = (function() + local candidates = { + { "fdfind", "-Htf", "-E", ".git" }, + { "fd", "-Htf", "-E", ".git" }, + { "rg", "-.g", "!.git", "--files" }, + } + ---@type string[]? + local cache + return function() + if not cache then + for _, candidate in ipairs(candidates) do + if vim.system then + if pcall(vim.system, { candidate[1], "--version" }) then + cache = candidate + break + end + elseif + pcall(function() + Job:new { command = candidate[1], args = { "--version" } } + end) + then + cache = candidate + break + end + end + end + return cache + end + end)(), +} ---@class FrecencyFinderConfig ---@field chunk_size? integer default: 1000 @@ -35,11 +66,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 +78,15 @@ Finder.new = function(database, entry_maker, need_scandir, path, state, finder_c closed = false, database = database, entry_maker = entry_maker, - path = path, + paths = paths, + process = {}, 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, @@ -77,44 +109,48 @@ end ---@param epoch? integer ---@return nil function Finder:start(epoch) - local ok + ---@type table + local results = {} if config.workspace_scan_cmd ~= "LUA" and self.need_scan_dir then - ---@type string[][] - local cmds = config.workspace_scan_cmd and { config.workspace_scan_cmd } - or { { "fdfind", "-Htf", "-E", ".git" }, { "fd", "-Htf", "-E", ".git" }, { "rg", "-.g", "!.git", "--files" } } - for _, c in ipairs(cmds) do - ok = self:scan_dir_cmd(c) - if ok then - log.debug("scan_dir_cmd: " .. vim.inspect(c)) - break + local cmd = config.workspace_scan_cmd --[=[@as string[]]=] + or Finder.cmd() + if cmd then + for _, path in ipairs(self.paths) do + log.debug(("scan_dir_cmd: %s: %s"):format(vim.inspect(cmd), path)) + results[path] = self:scan_dir_cmd(path, cmd) end end end 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) end self.tx.send(nil) - if self.need_scan_dir and not ok then - log.debug "scan_dir_lua" - async.util.scheduler() - self:scan_dir_lua() + if self.need_scan_dir then + for _, path in pairs(self.paths) do + if not results[path] then + log.debug("scan_dir_lua: " .. path) + async.util.scheduler() + self:scan_dir_lua(path) + end + end end end)() end +---@param path string ---@param cmd string[] ---@return boolean -function Finder:scan_dir_cmd(cmd) +function Finder:scan_dir_cmd(path, cmd) local function stdout(err, chunk) if not self.closed and not err and chunk then for name in chunk:gmatch "[^\n]+" do local cleaned = name:gsub("^%./", "") - local fullpath = os_util.join_path(self.path, cleaned) + local fullpath = os_util.join_path(path, cleaned) local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 } self.scan_tx.send(entry) end @@ -122,22 +158,31 @@ function Finder:scan_dir_cmd(cmd) end local function on_exit() - self.process = nil - self:close() - self.scan_tx.send(nil) + self.process[path] = { done = true } + local done_all = true + for _, p in ipairs(self.paths) do + if not self.process[p] or not self.process[p].done then + done_all = false + break + end + end + if done_all then + self:close() + self.scan_tx.send(nil) + end end - local ok + local ok, process if vim.system then ---@diagnostic disable-next-line: assign-type-mismatch - ok, self.process = pcall(vim.system, cmd, { - cwd = self.path, + ok, process = pcall(vim.system, cmd, { + cwd = path, text = true, stdout = stdout, }, on_exit) else -- for Neovim v0.9.x - ok, self.process = pcall(function() + ok, process = pcall(function() local args = {} for i, arg in ipairs(cmd) do if i > 1 then @@ -146,7 +191,7 @@ function Finder:scan_dir_cmd(cmd) end log.debug { cmd = cmd[1], args = args } local job = Job:new { - cwd = self.path, + cwd = path, command = cmd[1], args = args, on_stdout = stdout, @@ -156,21 +201,22 @@ function Finder:scan_dir_cmd(cmd) return job.handle end) end - if not ok then - self.process = nil + if ok then + self.process[path] = process--[[@as VimSystemObj]] end return ok end ---@async +---@param path string ---@return nil -function Finder:scan_dir_lua() +function Finder:scan_dir_lua(path) local count = 0 - for name in fs.scan_dir(self.path) do + for name in fs.scan_dir(path) do if self.closed then break end - local fullpath = os_util.join_path(self.path, name) + local fullpath = os_util.join_path(path, name) local entry = self.entry_maker { id = 0, count = 0, path = fullpath, score = 0 } self.scan_tx.send(entry) count = count + 1 @@ -213,7 +259,7 @@ end ---@param process_result fun(entry: FrecencyEntry): nil ---@param entries FrecencyEntry[] ---@return boolean? -function Finder:process_table(process_result, entries) +function Finder.process_table(_, process_result, entries) for _, entry in ipairs(entries) do if process_result(entry) then return true @@ -252,13 +298,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 @@ -275,8 +321,10 @@ end function Finder:close() self.closed = true - if self.process then - self.process:kill(9) + for _, process in pairs(self.process) do + if not process.done then + process.obj:kill(9) + end end end diff --git a/lua/frecency/klass.lua b/lua/frecency/klass.lua index 0635c72e..332231e5 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 5a0196df..9f8690ba 100644 --- a/lua/frecency/picker.lua +++ b/lua/frecency/picker.lua @@ -5,7 +5,6 @@ local config = require "frecency.config" local fs = require "frecency.fs" local fuzzy_sorter = require "frecency.fuzzy_sorter" local substr_sorter = require "frecency.substr_sorter" -local log = require "frecency.log" local lazy_require = require "frecency.lazy_require" local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]] local actions = lazy_require "telescope.actions" @@ -21,7 +20,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 = {} @@ -67,17 +66,17 @@ end ---@field workspace? 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 +90,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 +138,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 +150,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 +163,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 +191,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 +216,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