Merge pull request #290 from charmbracelet/charm-432

Kujtim Hoxha created

feat(tui): completions: add select and insert keybinds

Change summary

internal/tui/components/chat/editor/editor.go      |  8 ++-
internal/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(-)

Detailed changes

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:

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 {

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"),
+		),
 	}
 }
 

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