diff --git a/lua/snacks/explorer/tree.lua b/lua/snacks/explorer/tree.lua index 850c2ae10..6954167cb 100644 --- a/lua/snacks/explorer/tree.lua +++ b/lua/snacks/explorer/tree.lua @@ -1,6 +1,7 @@ ---@class snacks.picker.explorer.Node ---@field path string ---@field name string +---@field display_name? string compacted chain name (e.g. "src/main/java/com/example") ---@field hidden? boolean ---@field status? string merged git status ---@field dir_status? string git status of the directory @@ -14,6 +15,7 @@ ---@field utime? number ---@field children table ---@field severity? number +---@field _compact? {parent: snacks.picker.explorer.Node, display_name: string} transient compact-chain metadata ---@class snacks.picker.explorer.Filter ---@field hidden? boolean show hidden files @@ -174,15 +176,41 @@ function Tree:refresh(path) end, { all = true }) end +---@param node snacks.picker.explorer.Node start of the chain (a directory) +---@param filter fun(node: snacks.picker.explorer.Node):boolean +---@return snacks.picker.explorer.Node deepest, string[] segments +function Tree:compact_chain(node, filter) + local segments = { node.name } + local cur = node + while cur.dir do + if not cur.expanded then + self:expand(cur) + end + local single ---@type snacks.picker.explorer.Node? + local count = 0 + for _, c in pairs(cur.children) do + if filter(c) then + count = count + 1 + single = c + if count > 1 then + break + end + end + end + if count == 1 and single.dir then + cur = single + segments[#segments + 1] = cur.name + else + break + end + end + return cur, segments +end + ---@param node snacks.picker.explorer.Node ---@param fn fun(node: snacks.picker.explorer.Node):boolean? return `false` to not process children, `true` to abort ----@param opts? {all?: boolean} -function Tree:walk(node, fn, opts) - local abort = false ---@type boolean? - abort = fn(node) - if abort ~= nil then - return abort - end +---@param opts? {all?: boolean, compact?: boolean, filter?: fun(node: snacks.picker.explorer.Node):boolean} +function Tree:walk_children(node, fn, opts) local children = vim.tbl_values(node.children) ---@type snacks.picker.explorer.Node[] table.sort(children, function(a, b) if a.dir ~= b.dir then @@ -192,11 +220,35 @@ function Tree:walk(node, fn, opts) end) for c, child in ipairs(children) do child.last = c == #children - abort = false - if child.dir and (child.open or (opts and opts.all)) then - abort = self:walk(child, fn, opts) + local abort = false + if opts and opts.compact and opts.filter and child.dir then + if opts.filter(child) then + local deepest, segments = self:compact_chain(child, opts.filter) + if #segments >= 2 then + deepest.last = child.last + deepest._compact = { parent = node, display_name = table.concat(segments, "/") } + abort = fn(deepest) + if abort == nil and deepest.dir and (deepest.open or (opts and opts.all)) then + abort = self:walk_children(deepest, fn, opts) + end + else + child._compact = nil + if child.dir and (child.open or (opts and opts.all)) then + abort = self:walk(child, fn, opts) + else + abort = fn(child) + end + end + else + -- filtered out: skip, but mirror the normal "don't process children" behavior + child._compact = nil + end else - abort = fn(child) + if child.dir and (child.open or (opts and opts.all)) then + abort = self:walk(child, fn, opts) + else + abort = fn(child) + end end if abort then return true @@ -205,6 +257,17 @@ function Tree:walk(node, fn, opts) return false end +---@param node snacks.picker.explorer.Node +---@param fn fun(node: snacks.picker.explorer.Node):boolean? return `false` to not process children, `true` to abort +---@param opts? {all?: boolean, compact?: boolean, filter?: fun(node: snacks.picker.explorer.Node):boolean} +function Tree:walk(node, fn, opts) + local abort = fn(node) + if abort ~= nil then + return abort + end + return self:walk_children(node, fn, opts) +end + ---@param filter snacks.picker.explorer.Filter function Tree:filter(filter) local exclude = filter.exclude and #filter.exclude > 0 and Snacks.picker.util.globber(filter.exclude) @@ -246,7 +309,7 @@ function Tree:get(cwd, cb, opts) self:expand(n) end cb(n) - end) + end, { all = opts.all, expand = opts.expand, compact = opts.compact, filter = filter }) end ---@param cwd string diff --git a/lua/snacks/picker/config/sources.lua b/lua/snacks/picker/config/sources.lua index ece49592c..6b82ac21c 100644 --- a/lua/snacks/picker/config/sources.lua +++ b/lua/snacks/picker/config/sources.lua @@ -39,6 +39,7 @@ M.buffers = { ---@class snacks.picker.explorer.Config: snacks.picker.files.Config|{} ---@field follow_file? boolean follow the file from the current buffer ---@field tree? boolean show the file tree (default: true) +---@field compact_folders? boolean collapse single-child directory chains into one row (default: false) ---@field git_status? boolean show git status (default: true) ---@field git_status_open? boolean show recursive git status for open directories ---@field git_untracked? boolean needed to show untracked git status @@ -52,6 +53,7 @@ M.explorer = { sort = { fields = { "sort" } }, supports_live = true, tree = true, + compact_folders = false, watch = true, diagnostics = true, diagnostics_open = false, diff --git a/lua/snacks/picker/format.lua b/lua/snacks/picker/format.lua index 63a56a0df..961169f8b 100644 --- a/lua/snacks/picker/format.lua +++ b/lua/snacks/picker/format.lua @@ -87,9 +87,20 @@ function M.filename(item, picker) local dir_hl = "SnacksPickerDir" if picker.opts.formatters.file.filename_only then - path = vim.fn.fnamemodify(item.file, ":t") + path = item.display_name or vim.fn.fnamemodify(item.file, ":t") path = path == "" and item.file or path - ret[#ret + 1] = { path, base_hl, field = "file" } + if item.display_name then + -- render a compacted directory chain, dimming the path separators + local parts = vim.split(path, "/", { plain = true }) + for i, p in ipairs(parts) do + if i > 1 then + ret[#ret + 1] = { "/", dir_hl, field = "file" } + end + ret[#ret + 1] = { p, base_hl, field = "file" } + end + else + ret[#ret + 1] = { path, base_hl, field = "file" } + end else ret[#ret + 1] = { "", diff --git a/lua/snacks/picker/source/explorer.lua b/lua/snacks/picker/source/explorer.lua index 7679ce749..7032f1f01 100644 --- a/lua/snacks/picker/source/explorer.lua +++ b/lua/snacks/picker/source/explorer.lua @@ -19,6 +19,7 @@ local uv = vim.uv or vim.loop ---@field sort? string ---@field internal? boolean internal parent directories not part of fd output ---@field status? string +---@field display_name? string compacted chain name (e.g. "src/main/java/com/example") local function norm(path) return svim.fs.normalize(path) @@ -230,7 +231,9 @@ function M.explorer(opts, ctx) local top = Tree:find(ctx.filter.cwd) local last = {} ---@type table Tree:get(ctx.filter.cwd, function(node) - local parent = node.parent and items[node.parent.path] or nil + local compact = node._compact + local parent_node = compact and compact.parent or node.parent + local parent = parent_node and items[parent_node.path] or nil local status = node.status if not status and parent and parent.dir_status then status = parent.dir_status @@ -242,6 +245,7 @@ function M.explorer(opts, ctx) dir_status = node.dir_status or parent and parent.dir_status, text = node.path, parent = parent, + display_name = compact and compact.display_name or nil, hidden = node.hidden, ignored = node.ignored, status = (not node.dir or not node.open or opts.git_status_open) and status or nil, @@ -249,17 +253,23 @@ function M.explorer(opts, ctx) type = node.type, severity = (not node.dir or not node.open or opts.diagnostics_open) and node.severity or nil, } - if last[node.parent] then - last[node.parent].last = false + if last[parent_node] then + last[parent_node].last = false end - last[node.parent] = item + last[parent_node] = item if top == node then item.hidden = false item.ignored = false end items[node.path] = item cb(item) - end, { hidden = opts.hidden, ignored = opts.ignored, exclude = opts.exclude, include = opts.include }) + end, { + hidden = opts.hidden, + ignored = opts.ignored, + exclude = opts.exclude, + include = opts.include, + compact = opts.compact_folders, + }) end end