Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions lua/snacks/explorer/tree.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +15,7 @@
---@field utime? number
---@field children table<string, snacks.picker.explorer.Node>
---@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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lua/snacks/picker/config/sources.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,6 +53,7 @@ M.explorer = {
sort = { fields = { "sort" } },
supports_live = true,
tree = true,
compact_folders = false,
watch = true,
diagnostics = true,
diagnostics_open = false,
Expand Down
15 changes: 13 additions & 2 deletions lua/snacks/picker/format.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
"",
Expand Down
20 changes: 15 additions & 5 deletions lua/snacks/picker/source/explorer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -230,7 +231,9 @@ function M.explorer(opts, ctx)
local top = Tree:find(ctx.filter.cwd)
local last = {} ---@type table<snacks.picker.explorer.Node, snacks.picker.explorer.Item>
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
Expand All @@ -242,24 +245,31 @@ 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,
last = true,
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

Expand Down