From cc5ef2912d4001c1b3b9f478165fa8a56cb55bbc Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 23 Jul 2025 13:20:03 -0400 Subject: [PATCH] feat(tui): completions: add select and insert keybinds This adds keybinds to select next/previous completion item using ctrl+p and ctrl+n similar to Vim's completion behavior. --- internal/tui/components/chat/editor/editor.go | 8 +++-- .../tui/components/completions/completions.go | 27 +++++++++++++++- internal/tui/components/completions/keys.go | 10 ++++++ internal/tui/tui.go | 32 ++++++++----------- 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 242075d98e99da0117430a26df34357f58c18d10..55a5e7525a430039b314cd810cb94856185cf5af 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -187,9 +187,11 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { value = value[:m.completionsStartIndex] value += item.Path m.textarea.SetValue(value) - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 + if !msg.Insert { + m.isCompletionsOpen = false + m.currentQuery = "" + m.completionsStartIndex = 0 + } return m, nil } case openEditorMsg: diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index bd30dc394d47ad80421e8c78d3a0f84730518a9c..6c63afd22e982e5ba40f5d175fc71449bcd0879e 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -36,7 +36,8 @@ type CompletionsOpenedMsg struct{} type CloseCompletionsMsg struct{} type SelectCompletionMsg struct { - Value any // The value of the selected completion item + Value any // The value of the selected completion item + Insert bool } type Completions interface { @@ -115,6 +116,30 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { d, cmd := c.list.Update(msg) c.list = d.(list.ListModel) return c, cmd + case key.Matches(msg, c.keyMap.UpInsert): + selectedItemInx := c.list.SelectedIndex() - 1 + items := c.list.Items() + if selectedItemInx == list.NoSelection || selectedItemInx < 0 { + return c, nil // No item selected, do nothing + } + selectedItem := items[selectedItemInx].(CompletionItem).Value() + c.list.SetSelected(selectedItemInx) + return c, util.CmdHandler(SelectCompletionMsg{ + Value: selectedItem, + Insert: true, + }) + case key.Matches(msg, c.keyMap.DownInsert): + selectedItemInx := c.list.SelectedIndex() + 1 + items := c.list.Items() + if selectedItemInx == list.NoSelection || selectedItemInx >= len(items) { + return c, nil // No item selected, do nothing + } + selectedItem := items[selectedItemInx].(CompletionItem).Value() + c.list.SetSelected(selectedItemInx) + return c, util.CmdHandler(SelectCompletionMsg{ + Value: selectedItem, + Insert: true, + }) case key.Matches(msg, c.keyMap.Select): selectedItemInx := c.list.SelectedIndex() if selectedItemInx == list.NoSelection { diff --git a/internal/tui/components/completions/keys.go b/internal/tui/components/completions/keys.go index 530b429fe32ffd89d73c6cec1723c27de1ddd459..82372358028aec2b1384f1b4b6bff90be4a05eb8 100644 --- a/internal/tui/components/completions/keys.go +++ b/internal/tui/components/completions/keys.go @@ -9,6 +9,8 @@ type KeyMap struct { Up, Select, Cancel key.Binding + DownInsert, + UpInsert key.Binding } func DefaultKeyMap() KeyMap { @@ -29,6 +31,14 @@ func DefaultKeyMap() KeyMap { key.WithKeys("esc"), key.WithHelp("esc", "cancel"), ), + DownInsert: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "insert next"), + ), + UpInsert: key.NewBinding( + key.WithKeys("ctrl+p"), + key.WithHelp("ctrl+p", "insert previous"), + ), } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index dda0ce2b9a5190953cf2bc288001a74c8c763b09..22a2a52b92c52200d5ecc843c107d2ef33634a1b 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -319,26 +319,20 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { // handleKeyPressMsg processes keyboard input and routes to appropriate handlers. func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { + if a.completions.Open() { + // completions + keyMap := a.completions.KeyMap() + switch { + case key.Matches(msg, keyMap.Up), key.Matches(msg, keyMap.Down), + key.Matches(msg, keyMap.Select), key.Matches(msg, keyMap.Cancel), + key.Matches(msg, keyMap.UpInsert), key.Matches(msg, keyMap.DownInsert): + u, cmd := a.completions.Update(msg) + a.completions = u.(completions.Completions) + return cmd + } + } switch { - // completions - case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Up): - u, cmd := a.completions.Update(msg) - a.completions = u.(completions.Completions) - return cmd - - case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Down): - u, cmd := a.completions.Update(msg) - a.completions = u.(completions.Completions) - return cmd - case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Select): - u, cmd := a.completions.Update(msg) - a.completions = u.(completions.Completions) - return cmd - case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Cancel): - u, cmd := a.completions.Update(msg) - a.completions = u.(completions.Completions) - return cmd - // help + // help case key.Matches(msg, a.keyMap.Help): a.status.ToggleFullHelp() a.showingFullHelp = !a.showingFullHelp