Make the superproject selectable via the header

The header stands for the root repository: it is selected on startup,
shows the root's own state word, and the detail pane shows its state
like a submodule's (HEAD vs. upstream, changes, unpushed, leftover
submodule clones as untracked repositories). Submodules are ignored in
the root's changes; their rows show them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 11:54:40 +02:00
co-authored by Claude Sonnet 5.5
parent f9838c0c99
commit 72c060882b
18 changed files with 688 additions and 92 deletions
+106
View File
@@ -305,3 +305,109 @@ func equal(a, b []string) bool {
}
return true
}
// newTreeWithRoot is newTree with a selectable superproject.
func newTreeWithRoot(t *testing.T) *Model {
t.Helper()
m := New(theme.Default())
m.SetRoot(&submodule.Submodule{Name: "root"})
m.SetRoots(fixture())
m.SetSize(80, 20)
return &m
}
func TestRootIsSelectedInitially(t *testing.T) {
m := newTreeWithRoot(t)
if !m.RootSelected() {
t.Fatal("root not selected initially")
}
assertCursor(t, m, "")
}
func TestRootIsAboveFirstRow(t *testing.T) {
m := newTreeWithRoot(t)
m.Down()
assertCursor(t, m, "a")
if m.RootSelected() {
t.Error("root still selected after Down")
}
m.Up()
if !m.RootSelected() {
t.Error("Up from the first submodule should select the root")
}
m.Up() // stays
if !m.RootSelected() {
t.Error("Up on the root should stay on the root")
}
m.End()
assertCursor(t, m, "f")
m.Home()
if !m.RootSelected() {
t.Error("Home should select the root")
}
m.PageDown()
assertCursor(t, m, "f")
m.PageUp()
if !m.RootSelected() {
t.Error("PageUp should reach the root")
}
}
func TestRootNavigationKeys(t *testing.T) {
m := newTreeWithRoot(t)
m.Right() // like Down
assertCursor(t, m, "a")
m.Left() // top level, nothing to collapse: the parent is the root
if !m.RootSelected() {
t.Error("Left on a top-level submodule should select the root")
}
m.Left()
m.Toggle()
m.CollapseAll()
m.ExpandAll()
if !m.RootSelected() {
t.Error("fold keys must not move the root selection")
}
m.Down()
m.Down() // b
m.Right()
m.Right() // d
m.CollapseAll()
assertCursor(t, m, "b")
}
func TestReplaceRootsKeepsRootSelected(t *testing.T) {
m := newTreeWithRoot(t)
newRoot := &submodule.Submodule{Name: "root"}
m.SetRoot(newRoot)
m.ReplaceRoots(fixture())
if !m.RootSelected() || m.Selected() != newRoot {
t.Error("root selection should move to the reloaded root")
}
m.Down()
m.SetRoot(&submodule.Submodule{Name: "root"})
m.ReplaceRoots(fixture())
assertCursor(t, m, "a")
}
func TestFilterKeepsRoot(t *testing.T) {
m := newTreeWithRoot(t)
m.SetFilter("f")
if !m.RootSelected() {
t.Error("the root stays selected while filtering")
}
m.Down()
assertCursor(t, m, "f")
m.SetFilter("zzz") // nothing matches: the root remains, not nil
if !m.RootSelected() {
t.Error("no match should fall back to the root")
}
m.SetFilter("")
if !m.RootSelected() {
t.Error("closing the filter should keep the root")
}
}