summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorspoons-and-mirrors <[email protected]>2025-08-11 15:47:51 +0200
committerGitHub <[email protected]>2025-08-11 08:47:51 -0500
commitab2df0ae3356d9a034d432b53f96c8cf0b83bf1b (patch)
treed2584ee27ddb2165096985c0bc4749d4f886aa89
parent23757f3ac09b540f31d5df2088e65e5399d92f7f (diff)
downloadopencode-ab2df0ae3356d9a034d432b53f96c8cf0b83bf1b.tar.gz
opencode-ab2df0ae3356d9a034d432b53f96c8cf0b83bf1b.zip
Feat: Implement Wrap-Around Navigation for List Selection (for Models and Tools modal) (#1768)
-rw-r--r--packages/tui/internal/components/list/list.go29
-rw-r--r--packages/tui/internal/components/list/list_test.go51
2 files changed, 62 insertions, 18 deletions
diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go
index fd2d7d93f..a9823d0ab 100644
--- a/packages/tui/internal/components/list/list.go
+++ b/packages/tui/internal/components/list/list.go
@@ -173,7 +173,13 @@ func (c *listComponent[T]) moveUp() {
}
}
- // If no selectable item found above, stay at current position
+ // If no selectable item found above, wrap to the bottom
+ for i := len(c.items) - 1; i > c.selectedIdx; i-- {
+ if c.isSelectable(c.items[i]) {
+ c.selectedIdx = i
+ return
+ }
+ }
}
// moveDown moves the selection down, skipping non-selectable items
@@ -183,20 +189,19 @@ func (c *listComponent[T]) moveDown() {
}
originalIdx := c.selectedIdx
- for {
- if c.selectedIdx < len(c.items)-1 {
- c.selectedIdx++
- } else {
- break
- }
-
- if c.isSelectable(c.items[c.selectedIdx]) {
+ // First try moving down from current position
+ for i := c.selectedIdx + 1; i < len(c.items); i++ {
+ if c.isSelectable(c.items[i]) {
+ c.selectedIdx = i
return
}
+ }
- // Prevent infinite loop
- if c.selectedIdx == originalIdx {
- break
+ // If no selectable item found below, wrap to the top
+ for i := 0; i < originalIdx; i++ {
+ if c.isSelectable(c.items[i]) {
+ c.selectedIdx = i
+ return
}
}
}
diff --git a/packages/tui/internal/components/list/list_test.go b/packages/tui/internal/components/list/list_test.go
index 663503a4a..25cca8cf4 100644
--- a/packages/tui/internal/components/list/list_test.go
+++ b/packages/tui/internal/components/list/list_test.go
@@ -138,15 +138,18 @@ func TestCtrlNavigation(t *testing.T) {
func TestNavigationBoundaries(t *testing.T) {
list := createTestList()
- // Test up arrow at first item (should stay at 0)
+ // Test up arrow at first item (should wrap to last item)
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
updatedModel, _ := list.Update(upKey)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
- if idx != 0 {
- t.Errorf("Expected to stay at index 0 when pressing up at first item, got %d", idx)
+ if idx != 2 {
+ t.Errorf("Expected to wrap to index 2 when pressing up at first item, got %d", idx)
}
+ // Move to first item
+ list.SetSelectedIndex(0)
+
// Move to last item
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
updatedModel, _ = list.Update(downKey)
@@ -158,12 +161,12 @@ func TestNavigationBoundaries(t *testing.T) {
t.Errorf("Expected to be at index 2, got %d", idx)
}
- // Test down arrow at last item (should stay at 2)
+ // Test down arrow at last item (should wrap to first item)
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
- if idx != 2 {
- t.Errorf("Expected to stay at index 2 when pressing down at last item, got %d", idx)
+ if idx != 0 {
+ t.Errorf("Expected to wrap to index 0 when pressing down at last item, got %d", idx)
}
}
@@ -208,3 +211,39 @@ func TestEmptyList(t *testing.T) {
t.Error("Expected IsEmpty() to return true for empty list")
}
}
+
+func TestWrapAroundNavigation(t *testing.T) {
+ list := createTestList()
+
+ // Start at first item (index 0)
+ _, idx := list.GetSelectedItem()
+ if idx != 0 {
+ t.Errorf("Expected to start at index 0, got %d", idx)
+ }
+
+ // Press up arrow - should wrap to last item (index 2)
+ upKey := tea.KeyPressMsg{Code: tea.KeyUp}
+ updatedModel, _ := list.Update(upKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 2 {
+ t.Errorf("Expected to wrap to index 2 when pressing up from first item, got %d", idx)
+ }
+
+ // Press down arrow - should wrap to first item (index 0)
+ downKey := tea.KeyPressMsg{Code: tea.KeyDown}
+ updatedModel, _ = list.Update(downKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 0 {
+ t.Errorf("Expected to wrap to index 0 when pressing down from last item, got %d", idx)
+ }
+
+ // Navigate to middle and verify normal navigation still works
+ updatedModel, _ = list.Update(downKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 1 {
+ t.Errorf("Expected to move to index 1, got %d", idx)
+ }
+}