@@ -27,6 +27,12 @@ type OpenCompletionsMsg struct {
2727type FilterCompletionsMsg struct {
2828 Query string // The query to filter completions
2929 Reopen bool
30+ X int // X position for the completions popup
31+ Y int // Y position for the completions popup
32+}
33+
34+type RepositionCompletionsMsg struct {
35+ X, Y int
3036}
3137
3238type CompletionsClosedMsg struct{}
@@ -51,18 +57,24 @@ type Completions interface {
5157}
5258
5359type completionsCmp struct {
54− width int
55− height int // Height of the completions component`
56− x int // X position for the completions popup
57− y int // Y position for the completions popup
58− open bool // Indicates if the completions are open
59− keyMap KeyMap
60+ wWidth int // The window width
61+ wHeight int // The window height
62+ width int
63+ lastWidth int
64+ height int // Height of the completions component`
65+ x, xorig int // X position for the completions popup
66+ y int // Y position for the completions popup
67+ open bool // Indicates if the completions are open
68+ keyMap KeyMap
6069
6170 list list.ListModel
6271 query string // The current filter query
6372}
6473
65−const maxCompletionsWidth = 80 // Maximum width for the completions popup
74+const (
75+ maxCompletionsWidth = 80 // Maximum width for the completions popup
76+ minCompletionsWidth = 20 // Minimum width for the completions popup
77+)
6678
6779func New() Completions {
6880 completionsKeyMap := DefaultKeyMap()
@@ -83,7 +95,7 @@ func New() Completions {
8395 )
8496 return &completionsCmp{
8597 width: 0,
86− height: 0,
98+ height: maxCompletionsHeight,
8799 list: l,
88100 query: "",
89101 keyMap: completionsKeyMap,
@@ -102,8 +114,7 @@ func (c *completionsCmp) Init() tea.Cmd {
102114func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
103115 switch msg := msg.(type) {
104116 case tea.WindowSizeMsg:
105− c.width = min(msg.Width-c.x, maxCompletionsWidth)
106− c.height = min(msg.Height-c.y, 15)
117+ c.wWidth, c.wHeight = msg.Width, msg.Height
107118 return c, nil
108119 case tea.KeyPressMsg:
109120 switch {
@@ -154,13 +165,16 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154165 case key.Matches(msg, c.keyMap.Cancel):
155166 return c, util.CmdHandler(CloseCompletionsMsg{})
156167 }
168+ case RepositionCompletionsMsg:
169+ c.x, c.y = msg.X, msg.Y
170+ c.adjustPosition()
157171 case CloseCompletionsMsg:
158172 c.open = false
159173 return c, util.CmdHandler(CompletionsClosedMsg{})
160174 case OpenCompletionsMsg:
161175 c.open = true
162176 c.query = ""
163− c.x = msg.X
177+ c.x, c.xorig = msg.X, msg.X
164178 c.y = msg.Y
165179 items := []util.Model{}
166180 t := styles.CurrentTheme()
@@ -168,10 +182,18 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
168182 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
169183 items = append(items, item)
170184 }
171− c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height
185+ width := listWidth(items)
186+ if len(items) == 0 {
187+ width = listWidth(c.list.Items())
188+ }
189+ if c.x+width >= c.wWidth {
190+ c.x = c.wWidth - width - 1
191+ }
192+ c.width = width
193+ c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height
172194 return c, tea.Batch(
173− c.list.SetSize(c.width, c.height),
174195 c.list.SetItems(items),
196+ c.list.SetSize(c.width, c.height),
175197 util.CmdHandler(CompletionsOpenedMsg{}),
176198 )
177199 case FilterCompletionsMsg:
@@ -195,8 +217,11 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
195217 c.query = msg.Query
196218 var cmds []tea.Cmd
197219 cmds = append(cmds, c.list.Filter(msg.Query))
198− itemsLen := len(c.list.Items())
199− c.height = max(min(maxCompletionsHeight, itemsLen), 1)
220+ items := c.list.Items()
221+ itemsLen := len(items)
222+ c.xorig = msg.X
223+ c.x, c.y = msg.X, msg.Y
224+ c.adjustPosition()
200225 cmds = append(cmds, c.list.SetSize(c.width, c.height))
201226 if itemsLen == 0 {
202227 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
@@ -209,21 +234,54 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
209234 return c, nil
210235}
211236
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+
212251// View implements Completions.
213252func (c *completionsCmp) View() string {
214253 if !c.open || len(c.list.Items()) == 0 {
215254 return ""
216255 }
217256
218− return c.style().Render(c.list.View())
219−}
220−
221−func (c *completionsCmp) style() lipgloss.Style {
222257 t := styles.CurrentTheme()
223− return t.S().Base.
258+ style := t.S().Base.
224259 Width(c.width).
225260 Height(c.height).
226261 Background(t.BgSubtle)
262+
263+ return style.Render(c.list.View())
264+}
265+
266+// listWidth returns the width of the last 10 items in the list, which is used
267+// to determine the width of the completions popup.
268+// Note this only works for [completionItemCmp] items.
269+func listWidth[T any](items []T) int {
270+ var width int
271+ if len(items) == 0 {
272+ return width
273+ }
274+
275+ for i := len(items) - 1; i >= 0 && i >= len(items)-10; i-- {
276+ item, ok := any(items[i]).(*completionItemCmp)
277+ if !ok {
278+ continue
279+ }
280+ itemWidth := lipgloss.Width(item.text) + 2 // +2 for padding
281+ width = max(width, itemWidth)
282+ }
283+
284+ return width
227285}
228286
229287func (c *completionsCmp) Open() bool {