summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDorian Karter <[email protected]>2025-07-14 10:21:09 -0500
committerGitHub <[email protected]>2025-07-14 10:21:09 -0500
commit86a2ea44b5865921d9897d5cbfc27e3e3418f364 (patch)
treeaa4a47f5e03734df2081493cb6a9dd7dac9e1069 /packages
parenta2002c88c6f0bc75f52d30c3dfd6b5a83596f3b1 (diff)
downloadopencode-86a2ea44b5865921d9897d5cbfc27e3e3418f364.tar.gz
opencode-86a2ea44b5865921d9897d5cbfc27e3e3418f364.zip
feat(tui): add support for readline list nav (`ctrl-p`/`ctrl-n`) (#955)
Diffstat (limited to 'packages')
-rw-r--r--packages/tui/internal/components/list/list.go4
-rw-r--r--packages/tui/internal/components/list/list_test.go160
2 files changed, 162 insertions, 2 deletions
diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go
index 16bc73ca5..8a18d28fc 100644
--- a/packages/tui/internal/components/list/list.go
+++ b/packages/tui/internal/components/list/list.go
@@ -46,11 +46,11 @@ type listKeyMap struct {
var simpleListKeys = listKeyMap{
Up: key.NewBinding(
- key.WithKeys("up"),
+ key.WithKeys("up", "ctrl+p"),
key.WithHelp("↑", "previous list item"),
),
Down: key.NewBinding(
- key.WithKeys("down"),
+ key.WithKeys("down", "ctrl+n"),
key.WithHelp("↓", "next list item"),
),
UpAlpha: key.NewBinding(
diff --git a/packages/tui/internal/components/list/list_test.go b/packages/tui/internal/components/list/list_test.go
new file mode 100644
index 000000000..1e1240be2
--- /dev/null
+++ b/packages/tui/internal/components/list/list_test.go
@@ -0,0 +1,160 @@
+package list
+
+import (
+ "testing"
+
+ tea "github.com/charmbracelet/bubbletea/v2"
+)
+
+// testItem is a simple test implementation of ListItem
+type testItem struct {
+ value string
+}
+
+func (t testItem) Render(selected bool, width int) string {
+ return t.value
+}
+
+// createTestList creates a list with test items for testing
+func createTestList() *listComponent[testItem] {
+ items := []testItem{
+ {value: "item1"},
+ {value: "item2"},
+ {value: "item3"},
+ }
+ list := NewListComponent(items, 5, "empty", false)
+ return list.(*listComponent[testItem])
+}
+
+func TestArrowKeyNavigation(t *testing.T) {
+ list := createTestList()
+
+ // Test down arrow navigation
+ downKey := tea.KeyPressMsg{Code: tea.KeyDown}
+ updatedModel, _ := list.Update(downKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx := list.GetSelectedItem()
+ if idx != 1 {
+ t.Errorf("Expected selected index 1 after down arrow, got %d", idx)
+ }
+
+ // Test up arrow navigation
+ upKey := tea.KeyPressMsg{Code: tea.KeyUp}
+ updatedModel, _ = list.Update(upKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 0 {
+ t.Errorf("Expected selected index 0 after up arrow, got %d", idx)
+ }
+}
+
+func TestJKKeyNavigation(t *testing.T) {
+ items := []testItem{
+ {value: "item1"},
+ {value: "item2"},
+ {value: "item3"},
+ }
+ // Create list with alpha keys enabled
+ list := NewListComponent(items, 5, "empty", true).(*listComponent[testItem])
+
+ // Test j key (down)
+ jKey := tea.KeyPressMsg{Code: 'j', Text: "j"}
+ updatedModel, _ := list.Update(jKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx := list.GetSelectedItem()
+ if idx != 1 {
+ t.Errorf("Expected selected index 1 after 'j' key, got %d", idx)
+ }
+
+ // Test k key (up)
+ kKey := tea.KeyPressMsg{Code: 'k', Text: "k"}
+ updatedModel, _ = list.Update(kKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 0 {
+ t.Errorf("Expected selected index 0 after 'k' key, got %d", idx)
+ }
+}
+
+func TestCtrlNavigation(t *testing.T) {
+ list := createTestList()
+
+ // Test Ctrl-N (down)
+ ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
+ updatedModel, _ := list.Update(ctrlN)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx := list.GetSelectedItem()
+ if idx != 1 {
+ t.Errorf("Expected selected index 1 after Ctrl-N, got %d", idx)
+ }
+
+ // Test Ctrl-P (up)
+ ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
+ updatedModel, _ = list.Update(ctrlP)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 0 {
+ t.Errorf("Expected selected index 0 after Ctrl-P, got %d", idx)
+ }
+}
+
+func TestNavigationBoundaries(t *testing.T) {
+ list := createTestList()
+
+ // Test up arrow at first item (should stay at 0)
+ 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)
+ }
+
+ // Move to last item
+ downKey := tea.KeyPressMsg{Code: tea.KeyDown}
+ updatedModel, _ = list.Update(downKey)
+ list = updatedModel.(*listComponent[testItem])
+ updatedModel, _ = list.Update(downKey)
+ list = updatedModel.(*listComponent[testItem])
+ _, idx = list.GetSelectedItem()
+ if idx != 2 {
+ t.Errorf("Expected to be at index 2, got %d", idx)
+ }
+
+ // Test down arrow at last item (should stay at 2)
+ 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)
+ }
+}
+
+func TestEmptyList(t *testing.T) {
+ emptyList := NewListComponent([]testItem{}, 5, "empty", false).(*listComponent[testItem])
+
+ // Test navigation on empty list (should not crash)
+ downKey := tea.KeyPressMsg{Code: tea.KeyDown}
+ upKey := tea.KeyPressMsg{Code: tea.KeyUp}
+ ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
+ ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
+
+ updatedModel, _ := emptyList.Update(downKey)
+ emptyList = updatedModel.(*listComponent[testItem])
+ updatedModel, _ = emptyList.Update(upKey)
+ emptyList = updatedModel.(*listComponent[testItem])
+ updatedModel, _ = emptyList.Update(ctrlN)
+ emptyList = updatedModel.(*listComponent[testItem])
+ updatedModel, _ = emptyList.Update(ctrlP)
+ emptyList = updatedModel.(*listComponent[testItem])
+
+ // Verify empty list behavior
+ _, idx := emptyList.GetSelectedItem()
+ if idx != -1 {
+ t.Errorf("Expected index -1 for empty list, got %d", idx)
+ }
+
+ if !emptyList.IsEmpty() {
+ t.Error("Expected IsEmpty() to return true for empty list")
+ }
+}