diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 7f06e69a388e10a49cbb792d0f4b8d231613eb3b..4e5f0bc431eb466cea5c6c7d436234c7a5e8531b 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -162,9 +162,7 @@ func (m *editorCmp) send() tea.Cmd { } func (m *editorCmp) repositionCompletions() tea.Msg { - cur := m.textarea.Cursor() - x := cur.X + m.x // adjust for padding - y := cur.Y + m.y + 1 + x, y := m.completionsPosition() return completions.RepositionCompletionsMsg{X: x, Y: y} } @@ -191,32 +189,37 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } if item, ok := msg.Value.(FileCompletionItem); ok { + word := m.textarea.Word() // If the selected item is a file, insert its path into the textarea value := m.textarea.Value() - value = value[:m.completionsStartIndex] - value += item.Path + value = value[:m.completionsStartIndex] + // Remove the current query + item.Path + // Insert the file path + value[m.completionsStartIndex+len(word):] // Append the rest of the value + // XXX: This will always move the cursor to the end of the textarea. m.textarea.SetValue(value) + m.textarea.MoveToEnd() if !msg.Insert { m.isCompletionsOpen = false m.currentQuery = "" m.completionsStartIndex = 0 } - return m, nil } case openEditorMsg: m.textarea.SetValue(msg.Text) m.textarea.MoveToEnd() case tea.KeyPressMsg: + cur := m.textarea.Cursor() + curIdx := m.textarea.Width()*cur.Y + cur.X switch { // Completions case msg.String() == "/" && !m.isCompletionsOpen && - // only show if beginning of prompt, or if previous char is a space: - (len(m.textarea.Value()) == 0 || m.textarea.Value()[len(m.textarea.Value())-1] == ' '): + // only show if beginning of prompt, or if previous char is a space or newline: + (len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))): m.isCompletionsOpen = true m.currentQuery = "" - m.completionsStartIndex = len(m.textarea.Value()) + m.completionsStartIndex = curIdx cmds = append(cmds, m.startCompletions) - case m.isCompletionsOpen && m.textarea.Cursor().X <= m.completionsStartIndex: + case m.isCompletionsOpen && curIdx <= m.completionsStartIndex: cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) } if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { @@ -253,6 +256,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if key.Matches(msg, m.keyMap.Newline) { m.textarea.InsertRune('\n') + cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) } // Handle Enter key if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) { @@ -284,12 +288,18 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // XXX: wont' work if editing in the middle of the field. m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word) m.currentQuery = word[1:] + x, y := m.completionsPosition() + x -= len(m.currentQuery) m.isCompletionsOpen = true - cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{ - Query: m.currentQuery, - Reopen: m.isCompletionsOpen, - })) - } else { + cmds = append(cmds, + util.CmdHandler(completions.FilterCompletionsMsg{ + Query: m.currentQuery, + Reopen: m.isCompletionsOpen, + X: x, + Y: y, + }), + ) + } else if m.isCompletionsOpen { m.isCompletionsOpen = false m.currentQuery = "" m.completionsStartIndex = 0 @@ -302,6 +312,16 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *editorCmp) completionsPosition() (int, int) { + cur := m.textarea.Cursor() + if cur == nil { + return m.x, m.y + 1 // adjust for padding + } + x := cur.X + m.x + y := cur.Y + m.y + 1 // adjust for padding + return x, y +} + func (m *editorCmp) Cursor() *tea.Cursor { cursor := m.textarea.Cursor() if cursor != nil { @@ -382,9 +402,7 @@ func (m *editorCmp) startCompletions() tea.Msg { }) } - cur := m.textarea.Cursor() - x := cur.X + m.x // adjust for padding - y := cur.Y + m.y + 1 + x, y := m.completionsPosition() return completions.OpenCompletionsMsg{ Completions: completionItems, X: x, diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index ab29d900010bb2a80e4e2b7d6135e44f6486769c..aad5dc8c83c163712a4d9b56e7a6442ce2380f25 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -27,6 +27,8 @@ type OpenCompletionsMsg struct { type FilterCompletionsMsg struct { Query string // The query to filter completions Reopen bool + X int // X position for the completions popup + Y int // Y position for the completions popup } type RepositionCompletionsMsg struct { @@ -165,6 +167,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case RepositionCompletionsMsg: c.x, c.y = msg.X, msg.Y + c.adjustPosition() case CloseCompletionsMsg: c.open = false return c, util.CmdHandler(CompletionsClosedMsg{}) @@ -216,15 +219,9 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, c.list.Filter(msg.Query)) items := c.list.Items() itemsLen := len(items) - width := listWidth(items) - c.lastWidth = c.width - if c.x < 0 || width < c.lastWidth { - c.x = c.xorig - } else if c.x+width >= c.wWidth { - c.x = c.wWidth - width - 1 - } - c.width = width - c.height = max(min(maxCompletionsHeight, itemsLen), 1) + c.xorig = msg.X + c.x, c.y = msg.X, msg.Y + c.adjustPosition() cmds = append(cmds, c.list.SetSize(c.width, c.height)) if itemsLen == 0 { cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{})) @@ -237,6 +234,20 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return c, nil } +func (c *completionsCmp) adjustPosition() { + items := c.list.Items() + itemsLen := len(items) + width := listWidth(items) + c.lastWidth = c.width + if c.x < 0 || width < c.lastWidth { + c.x = c.xorig + } else if c.x+width >= c.wWidth { + c.x = c.wWidth - width - 1 + } + c.width = width + c.height = max(min(maxCompletionsHeight, itemsLen), 1) +} + // View implements Completions. func (c *completionsCmp) View() string { if !c.open || len(c.list.Items()) == 0 { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 1cdc0c38243da39b2bd8c8eb276beea78f1dd37f..770e7b26945e9bf7109f3076e1ad95a1f24aa51a 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -116,7 +116,8 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, a.handleWindowResize(msg.Width, msg.Height) // Completions messages - case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg: + case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, + completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg: u, completionCmd := a.completions.Update(msg) a.completions = u.(completions.Completions) return a, completionCmd