Detailed changes
@@ -161,10 +161,17 @@ func (m *editorCmp) send() tea.Cmd {
)
}
+func (m *editorCmp) repositionCompletions() tea.Msg {
+ x, y := m.completionsPosition()
+ return completions.RepositionCompletionsMsg{X: x, Y: y}
+}
+
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ return m, m.repositionCompletions
case filepicker.FilePickedMsg:
if len(m.attachments) >= maxAttachments {
return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
@@ -182,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) {
@@ -244,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) {
@@ -275,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
@@ -293,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 {
@@ -373,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,
@@ -27,6 +27,12 @@ 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 {
+ X, Y int
}
type CompletionsClosedMsg struct{}
@@ -51,18 +57,24 @@ type Completions interface {
}
type completionsCmp struct {
- width int
- height int // Height of the completions component`
- x int // X position for the completions popup
- y int // Y position for the completions popup
- open bool // Indicates if the completions are open
- keyMap KeyMap
+ wWidth int // The window width
+ wHeight int // The window height
+ width int
+ lastWidth int
+ height int // Height of the completions component`
+ x, xorig int // X position for the completions popup
+ y int // Y position for the completions popup
+ open bool // Indicates if the completions are open
+ keyMap KeyMap
list list.ListModel
query string // The current filter query
}
-const maxCompletionsWidth = 80 // Maximum width for the completions popup
+const (
+ maxCompletionsWidth = 80 // Maximum width for the completions popup
+ minCompletionsWidth = 20 // Minimum width for the completions popup
+)
func New() Completions {
completionsKeyMap := DefaultKeyMap()
@@ -83,7 +95,7 @@ func New() Completions {
)
return &completionsCmp{
width: 0,
- height: 0,
+ height: maxCompletionsHeight,
list: l,
query: "",
keyMap: completionsKeyMap,
@@ -102,8 +114,7 @@ func (c *completionsCmp) Init() tea.Cmd {
func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
- c.width = min(msg.Width-c.x, maxCompletionsWidth)
- c.height = min(msg.Height-c.y, 15)
+ c.wWidth, c.wHeight = msg.Width, msg.Height
return c, nil
case tea.KeyPressMsg:
switch {
@@ -154,13 +165,16 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, c.keyMap.Cancel):
return c, util.CmdHandler(CloseCompletionsMsg{})
}
+ case RepositionCompletionsMsg:
+ c.x, c.y = msg.X, msg.Y
+ c.adjustPosition()
case CloseCompletionsMsg:
c.open = false
return c, util.CmdHandler(CompletionsClosedMsg{})
case OpenCompletionsMsg:
c.open = true
c.query = ""
- c.x = msg.X
+ c.x, c.xorig = msg.X, msg.X
c.y = msg.Y
items := []util.Model{}
t := styles.CurrentTheme()
@@ -168,10 +182,18 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
items = append(items, item)
}
- c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height
+ width := listWidth(items)
+ if len(items) == 0 {
+ width = listWidth(c.list.Items())
+ }
+ if c.x+width >= c.wWidth {
+ c.x = c.wWidth - width - 1
+ }
+ c.width = width
+ c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height
return c, tea.Batch(
- c.list.SetSize(c.width, c.height),
c.list.SetItems(items),
+ c.list.SetSize(c.width, c.height),
util.CmdHandler(CompletionsOpenedMsg{}),
)
case FilterCompletionsMsg:
@@ -195,8 +217,11 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
c.query = msg.Query
var cmds []tea.Cmd
cmds = append(cmds, c.list.Filter(msg.Query))
- itemsLen := len(c.list.Items())
- c.height = max(min(maxCompletionsHeight, itemsLen), 1)
+ items := c.list.Items()
+ itemsLen := len(items)
+ 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{}))
@@ -209,21 +234,54 @@ 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 {
return ""
}
- return c.style().Render(c.list.View())
-}
-
-func (c *completionsCmp) style() lipgloss.Style {
t := styles.CurrentTheme()
- return t.S().Base.
+ style := t.S().Base.
Width(c.width).
Height(c.height).
Background(t.BgSubtle)
+
+ return style.Render(c.list.View())
+}
+
+// listWidth returns the width of the last 10 items in the list, which is used
+// to determine the width of the completions popup.
+// Note this only works for [completionItemCmp] items.
+func listWidth[T any](items []T) int {
+ var width int
+ if len(items) == 0 {
+ return width
+ }
+
+ for i := len(items) - 1; i >= 0 && i >= len(items)-10; i-- {
+ item, ok := any(items[i]).(*completionItemCmp)
+ if !ok {
+ continue
+ }
+ itemWidth := lipgloss.Width(item.text) + 2 // +2 for padding
+ width = max(width, itemWidth)
+ }
+
+ return width
}
func (c *completionsCmp) Open() bool {
@@ -165,7 +165,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.keyboardEnhancements = msg
return p, nil
case tea.WindowSizeMsg:
- return p, p.SetSize(msg.Width, msg.Height)
+ u, cmd := p.editor.Update(msg)
+ p.editor = u.(editor.Editor)
+ return p, tea.Batch(p.SetSize(msg.Width, msg.Height), cmd)
case CancelTimerExpiredMsg:
p.isCanceling = false
return p, nil
@@ -118,19 +118,10 @@ 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:
+ case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg,
+ completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
u, completionCmd := a.completions.Update(msg)
a.completions = u.(completions.Completions)
- switch msg := msg.(type) {
- case completions.OpenCompletionsMsg:
- x, _ := a.completions.Position()
- if a.completions.Width()+x >= a.wWidth {
- // Adjust X position to fit in the window.
- msg.X = a.wWidth - a.completions.Width() - 1
- u, completionCmd = a.completions.Update(msg)
- a.completions = u.(completions.Completions)
- }
- }
return a, completionCmd
// Dialog messages