on main help sign in
fix(tui): completions: improve positioning and handling completions
97290c8Ayman Bagabas
internal/tui/components/chat/editor/editor.go+36−18
@@ -162,9 +162,7 @@ func (m *editorCmp) send() tea.Cmd {
162}
162}
163
163
164func (m *editorCmp) repositionCompletions() tea.Msg {
164func (m *editorCmp) repositionCompletions() tea.Msg {
165− cur := m.textarea.Cursor()
165+ x, y := m.completionsPosition()
166− x := cur.X + m.x // adjust for padding
167− y := cur.Y + m.y + 1
168 return completions.RepositionCompletionsMsg{X: x, Y: y}
166 return completions.RepositionCompletionsMsg{X: x, Y: y}
169}
167}
170
168
@@ -191,32 +189,37 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
191 return m, nil
189 return m, nil
192 }
190 }
193 if item, ok := msg.Value.(FileCompletionItem); ok {
191 if item, ok := msg.Value.(FileCompletionItem); ok {
192+ word := m.textarea.Word()
194 // If the selected item is a file, insert its path into the textarea
193 // If the selected item is a file, insert its path into the textarea
195 value := m.textarea.Value()
194 value := m.textarea.Value()
196− value = value[:m.completionsStartIndex]
195+ value = value[:m.completionsStartIndex] + // Remove the current query
197− value += item.Path
196+ item.Path + // Insert the file path
197+ value[m.completionsStartIndex+len(word):] // Append the rest of the value
198+ // XXX: This will always move the cursor to the end of the textarea.
198 m.textarea.SetValue(value)
199 m.textarea.SetValue(value)
200+ m.textarea.MoveToEnd()
199 if !msg.Insert {
201 if !msg.Insert {
200 m.isCompletionsOpen = false
202 m.isCompletionsOpen = false
201 m.currentQuery = ""
203 m.currentQuery = ""
202 m.completionsStartIndex = 0
204 m.completionsStartIndex = 0
203 }
205 }
204− return m, nil
205 }
206 }
206 case openEditorMsg:
207 case openEditorMsg:
207 m.textarea.SetValue(msg.Text)
208 m.textarea.SetValue(msg.Text)
208 m.textarea.MoveToEnd()
209 m.textarea.MoveToEnd()
209 case tea.KeyPressMsg:
210 case tea.KeyPressMsg:
211+ cur := m.textarea.Cursor()
212+ curIdx := m.textarea.Width()*cur.Y + cur.X
210 switch {
213 switch {
211 // Completions
214 // Completions
212 case msg.String() == "/" && !m.isCompletionsOpen &&
215 case msg.String() == "/" && !m.isCompletionsOpen &&
213− // only show if beginning of prompt, or if previous char is a space:
216+ // only show if beginning of prompt, or if previous char is a space or newline:
214− (len(m.textarea.Value()) == 0 || m.textarea.Value()[len(m.textarea.Value())-1] == ' '):
217+ (len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))):
215 m.isCompletionsOpen = true
218 m.isCompletionsOpen = true
216 m.currentQuery = ""
219 m.currentQuery = ""
217− m.completionsStartIndex = len(m.textarea.Value())
220+ m.completionsStartIndex = curIdx
218 cmds = append(cmds, m.startCompletions)
221 cmds = append(cmds, m.startCompletions)
219− case m.isCompletionsOpen && m.textarea.Cursor().X <= m.completionsStartIndex:
222+ case m.isCompletionsOpen && curIdx <= m.completionsStartIndex:
220 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
223 cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
221 }
224 }
222 if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
225 if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
@@ -253,6 +256,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
253 }
256 }
254 if key.Matches(msg, m.keyMap.Newline) {
257 if key.Matches(msg, m.keyMap.Newline) {
255 m.textarea.InsertRune('\n')
258 m.textarea.InsertRune('\n')
259+ cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
256 }
260 }
257 // Handle Enter key
261 // Handle Enter key
258 if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) {
262 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) {
284 // XXX: wont' work if editing in the middle of the field.
288 // XXX: wont' work if editing in the middle of the field.
285 m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word)
289 m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word)
286 m.currentQuery = word[1:]
290 m.currentQuery = word[1:]
291+ x, y := m.completionsPosition()
292+ x -= len(m.currentQuery)
287 m.isCompletionsOpen = true
293 m.isCompletionsOpen = true
294+ cmds = append(cmds,
288− cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{
295+ util.CmdHandler(completions.FilterCompletionsMsg{
289− Query: m.currentQuery,
296+ Query: m.currentQuery,
290− Reopen: m.isCompletionsOpen,
297+ Reopen: m.isCompletionsOpen,
298+ X: x,
299+ Y: y,
291− }))
300+ }),
292− } else {
301+ )
302+ } else if m.isCompletionsOpen {
293 m.isCompletionsOpen = false
303 m.isCompletionsOpen = false
294 m.currentQuery = ""
304 m.currentQuery = ""
295 m.completionsStartIndex = 0
305 m.completionsStartIndex = 0
@@ -302,6 +312,16 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
302 return m, tea.Batch(cmds...)
312 return m, tea.Batch(cmds...)
303}
313}
304
314
315+func (m *editorCmp) completionsPosition() (int, int) {
316+ cur := m.textarea.Cursor()
317+ if cur == nil {
318+ return m.x, m.y + 1 // adjust for padding
319+ }
320+ x := cur.X + m.x
321+ y := cur.Y + m.y + 1 // adjust for padding
322+ return x, y
323+}
324+
305func (m *editorCmp) Cursor() *tea.Cursor {
325func (m *editorCmp) Cursor() *tea.Cursor {
306 cursor := m.textarea.Cursor()
326 cursor := m.textarea.Cursor()
307 if cursor != nil {
327 if cursor != nil {
@@ -382,9 +402,7 @@ func (m *editorCmp) startCompletions() tea.Msg {
382 })
402 })
383 }
403 }
384
404
385− cur := m.textarea.Cursor()
405+ x, y := m.completionsPosition()
386− x := cur.X + m.x // adjust for padding
387− y := cur.Y + m.y + 1
388 return completions.OpenCompletionsMsg{
406 return completions.OpenCompletionsMsg{
389 Completions: completionItems,
407 Completions: completionItems,
390 X: x,
408 X: x,
internal/tui/components/completions/completions.go+20−9
@@ -27,6 +27,8 @@ type OpenCompletionsMsg struct {
27type FilterCompletionsMsg struct {
27type FilterCompletionsMsg struct {
28 Query string // The query to filter completions
28 Query string // The query to filter completions
29 Reopen bool
29 Reopen bool
30+ X int // X position for the completions popup
31+ Y int // Y position for the completions popup
30}
32}
31
33
32type RepositionCompletionsMsg struct {
34type RepositionCompletionsMsg struct {
@@ -165,6 +167,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
165 }
167 }
166 case RepositionCompletionsMsg:
168 case RepositionCompletionsMsg:
167 c.x, c.y = msg.X, msg.Y
169 c.x, c.y = msg.X, msg.Y
170+ c.adjustPosition()
168 case CloseCompletionsMsg:
171 case CloseCompletionsMsg:
169 c.open = false
172 c.open = false
170 return c, util.CmdHandler(CompletionsClosedMsg{})
173 return c, util.CmdHandler(CompletionsClosedMsg{})
@@ -216,15 +219,9 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
216 cmds = append(cmds, c.list.Filter(msg.Query))
219 cmds = append(cmds, c.list.Filter(msg.Query))
217 items := c.list.Items()
220 items := c.list.Items()
218 itemsLen := len(items)
221 itemsLen := len(items)
219− width := listWidth(items)
220− c.lastWidth = c.width
221− if c.x < 0 || width < c.lastWidth {
222− c.x = c.xorig
222+ c.xorig = msg.X
223− } else if c.x+width >= c.wWidth {
223+ c.x, c.y = msg.X, msg.Y
224− c.x = c.wWidth - width - 1
224+ c.adjustPosition()
225− }
226− c.width = width
227− c.height = max(min(maxCompletionsHeight, itemsLen), 1)
228 cmds = append(cmds, c.list.SetSize(c.width, c.height))
225 cmds = append(cmds, c.list.SetSize(c.width, c.height))
229 if itemsLen == 0 {
226 if itemsLen == 0 {
230 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
227 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
@@ -237,6 +234,20 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
237 return c, nil
234 return c, nil
238}
235}
239
236
237+func (c *completionsCmp) adjustPosition() {
238+ items := c.list.Items()
239+ itemsLen := len(items)
240+ width := listWidth(items)
241+ c.lastWidth = c.width
242+ if c.x < 0 || width < c.lastWidth {
243+ c.x = c.xorig
244+ } else if c.x+width >= c.wWidth {
245+ c.x = c.wWidth - width - 1
246+ }
247+ c.width = width
248+ c.height = max(min(maxCompletionsHeight, itemsLen), 1)
249+}
250+
240// View implements Completions.
251// View implements Completions.
241func (c *completionsCmp) View() string {
252func (c *completionsCmp) View() string {
242 if !c.open || len(c.list.Items()) == 0 {
253 if !c.open || len(c.list.Items()) == 0 {
internal/tui/tui.go+2−1
@@ -116,7 +116,8 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116 return a, a.handleWindowResize(msg.Width, msg.Height)
116 return a, a.handleWindowResize(msg.Width, msg.Height)
117
117
118 // Completions messages
118 // Completions messages
119+ case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg,
119− case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
120+ completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
120 u, completionCmd := a.completions.Update(msg)
121 u, completionCmd := a.completions.Update(msg)
121 a.completions = u.(completions.Completions)
122 a.completions = u.(completions.Completions)
122 return a, completionCmd
123 return a, completionCmd