@@ -27,6 +27,12 @@ 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
34+type RepositionCompletionsMsg struct {
32type CompletionsClosedMsg struct{}
38type CompletionsClosedMsg struct{}
@@ -51,18 +57,24 @@ type Completions interface {
53type completionsCmp struct {
59type completionsCmp struct {
54− width int
60+ wWidth int // The window width
61+ wHeight int // The window height
55− height int // Height of the completions component`
64+ height int // Height of the completions component`
56− x int // X position for the completions popup
65+ x, xorig int // X position for the completions popup
57− y int // Y position for the completions popup
66+ y int // Y position for the completions popup
58− open bool // Indicates if the completions are open
67+ open bool // Indicates if the completions are open
59− keyMap KeyMap
68+ keyMap KeyMap
61 list list.ListModel
70 list list.ListModel
62 query string // The current filter query
71 query string // The current filter query
65−const maxCompletionsWidth = 80 // Maximum width for the completions popup
75+ maxCompletionsWidth = 80 // Maximum width for the completions popup
76+ minCompletionsWidth = 20 // Minimum width for the completions popup
67func New() Completions {
79func New() Completions {
68 completionsKeyMap := DefaultKeyMap()
80 completionsKeyMap := DefaultKeyMap()
@@ -83,7 +95,7 @@ func New() Completions {
84 return &completionsCmp{
96 return &completionsCmp{
86− height: 0,
98+ height: maxCompletionsHeight,
88 query: "",
100 query: "",
89 keyMap: completionsKeyMap,
101 keyMap: completionsKeyMap,
@@ -102,8 +114,7 @@ func (c *completionsCmp) Init() tea.Cmd {
102func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
103 switch msg := msg.(type) {
115 switch msg := msg.(type) {
104 case tea.WindowSizeMsg:
116 case tea.WindowSizeMsg:
105− c.width = min(msg.Width-c.x, maxCompletionsWidth)
117+ c.wWidth, c.wHeight = msg.Width, msg.Height
106− c.height = min(msg.Height-c.y, 15)
107 return c, nil
118 return c, nil
108 case tea.KeyPressMsg:
119 case tea.KeyPressMsg:
@@ -154,13 +165,16 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154 case key.Matches(msg, c.keyMap.Cancel):
165 case key.Matches(msg, c.keyMap.Cancel):
155 return c, util.CmdHandler(CloseCompletionsMsg{})
166 return c, util.CmdHandler(CloseCompletionsMsg{})
168+ case RepositionCompletionsMsg:
169+ c.x, c.y = msg.X, msg.Y
157 case CloseCompletionsMsg:
171 case CloseCompletionsMsg:
158 c.open = false
172 c.open = false
159 return c, util.CmdHandler(CompletionsClosedMsg{})
173 return c, util.CmdHandler(CompletionsClosedMsg{})
160 case OpenCompletionsMsg:
174 case OpenCompletionsMsg:
161 c.open = true
175 c.open = true
162 c.query = ""
176 c.query = ""
163− c.x = msg.X
177+ c.x, c.xorig = msg.X, msg.X
164 c.y = msg.Y
178 c.y = msg.Y
165 items := []util.Model{}
179 items := []util.Model{}
166 t := styles.CurrentTheme()
180 t := styles.CurrentTheme()
@@ -168,10 +182,18 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
168 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
182 item := NewCompletionItem(completion.Title, completion.Value, WithBackgroundColor(t.BgSubtle))
169 items = append(items, item)
183 items = append(items, item)
185+ width := listWidth(items)
186+ if len(items) == 0 {
187+ width = listWidth(c.list.Items())
189+ if c.x+width >= c.wWidth {
190+ c.x = c.wWidth - width - 1
171− c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height
193+ c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height
172 return c, tea.Batch(
194 return c, tea.Batch(
173− c.list.SetSize(c.width, c.height),
174 c.list.SetItems(items),
195 c.list.SetItems(items),
196+ c.list.SetSize(c.width, c.height),
175 util.CmdHandler(CompletionsOpenedMsg{}),
197 util.CmdHandler(CompletionsOpenedMsg{}),
177 case FilterCompletionsMsg:
199 case FilterCompletionsMsg:
@@ -195,8 +217,11 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
195 c.query = msg.Query
217 c.query = msg.Query
196 var cmds []tea.Cmd
218 var cmds []tea.Cmd
197 cmds = append(cmds, c.list.Filter(msg.Query))
219 cmds = append(cmds, c.list.Filter(msg.Query))
198− itemsLen := len(c.list.Items())
220+ items := c.list.Items()
199− c.height = max(min(maxCompletionsHeight, itemsLen), 1)
221+ itemsLen := len(items)
223+ c.x, c.y = msg.X, msg.Y
200 cmds = append(cmds, c.list.SetSize(c.width, c.height))
225 cmds = append(cmds, c.list.SetSize(c.width, c.height))
201 if itemsLen == 0 {
226 if itemsLen == 0 {
202 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
227 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
@@ -209,21 +234,54 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
209 return c, nil
234 return c, nil
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 {
244+ } else if c.x+width >= c.wWidth {
245+ c.x = c.wWidth - width - 1
248+ c.height = max(min(maxCompletionsHeight, itemsLen), 1)
212// View implements Completions.
251// View implements Completions.
213func (c *completionsCmp) View() string {
252func (c *completionsCmp) View() string {
214 if !c.open || len(c.list.Items()) == 0 {
253 if !c.open || len(c.list.Items()) == 0 {
215 return ""
254 return ""
218− return c.style().Render(c.list.View())
221−func (c *completionsCmp) style() lipgloss.Style {
222 t := styles.CurrentTheme()
257 t := styles.CurrentTheme()
223− return t.S().Base.
258+ style := t.S().Base.
224 Width(c.width).
259 Width(c.width).
225 Height(c.height).
260 Height(c.height).
226 Background(t.BgSubtle)
261 Background(t.BgSubtle)
263+ return style.Render(c.list.View())
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 {
271+ if len(items) == 0 {
275+ for i := len(items) - 1; i >= 0 && i >= len(items)-10; i-- {
276+ item, ok := any(items[i]).(*completionItemCmp)
280+ itemWidth := lipgloss.Width(item.text) + 2 // +2 for padding
281+ width = max(width, itemWidth)
229func (c *completionsCmp) Open() bool {
287func (c *completionsCmp) Open() bool {