Archived
A text input above the footer filters submodules by path (case-insensitive). Matches keep their ancestors. A filtered-out selection moves to the next visible submodule; esc closes the input and removes the filter. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
69 lines
1.5 KiB
Go
69 lines
1.5 KiB
Go
package tree
|
|
|
|
import (
|
|
"path"
|
|
|
|
"submarine/internal/submodule"
|
|
)
|
|
|
|
type rowKind int
|
|
|
|
const (
|
|
rowDir rowKind = iota // non-selectable directory header, e.g. "modules/"
|
|
rowNode // a submodule
|
|
)
|
|
|
|
type row struct {
|
|
kind rowKind
|
|
depth int
|
|
label string // directory label for rowDir
|
|
node *submodule.Submodule
|
|
}
|
|
|
|
// rows flattens the visible part of the tree.
|
|
func (m *Model) rows() []row {
|
|
var out []row
|
|
m.appendRows(&out, m.roots, 0)
|
|
return out
|
|
}
|
|
|
|
// appendRows groups mods by the directory of their RelPath. Each group with a
|
|
// directory gets a header row; its submodules are indented one level deeper.
|
|
func (m *Model) appendRows(out *[]row, mods []*submodule.Submodule, depth int) {
|
|
for _, g := range groupByDir(m.filtered(mods)) {
|
|
d := depth
|
|
if g.dir != "." {
|
|
*out = append(*out, row{kind: rowDir, depth: depth, label: g.dir + "/"})
|
|
d++
|
|
}
|
|
for _, s := range g.mods {
|
|
*out = append(*out, row{kind: rowNode, depth: d, node: s})
|
|
if m.isExpanded(s) && len(s.Children) > 0 {
|
|
m.appendRows(out, s.Children, d+1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type dirGroup struct {
|
|
dir string
|
|
mods []*submodule.Submodule
|
|
}
|
|
|
|
// groupByDir keeps the order in which directories first appear.
|
|
func groupByDir(mods []*submodule.Submodule) []dirGroup {
|
|
var groups []dirGroup
|
|
idx := map[string]int{}
|
|
for _, s := range mods {
|
|
dir := path.Dir(s.RelPath)
|
|
i, ok := idx[dir]
|
|
if !ok {
|
|
i = len(groups)
|
|
idx[dir] = i
|
|
groups = append(groups, dirGroup{dir: dir})
|
|
}
|
|
groups[i].mods = append(groups[i].mods, s)
|
|
}
|
|
return groups
|
|
}
|