@@ -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{}
@@ -53,18 +59,24 @@ type Completions interface {
53type listModel = list.FilterableList[list.CompletionItem[any]]
59type listModel = list.FilterableList[list.CompletionItem[any]]
55type completionsCmp struct {
61type completionsCmp struct {
56− width int
62+ wWidth int // The window width
63+ wHeight int // The window height
57− height int // Height of the completions component`
66+ height int // Height of the completions component`
58− x int // X position for the completions popup
67+ x, xorig int // X position for the completions popup
59− y int // Y position for the completions popup
68+ y int // Y position for the completions popup
60− open bool // Indicates if the completions are open
69+ open bool // Indicates if the completions are open
61− keyMap KeyMap
70+ keyMap KeyMap
63 list listModel
72 list listModel
64 query string // The current filter query
73 query string // The current filter query
67−const maxCompletionsWidth = 80 // Maximum width for the completions popup
77+ maxCompletionsWidth = 80 // Maximum width for the completions popup
78+ minCompletionsWidth = 20 // Minimum width for the completions popup
69func New() Completions {
81func New() Completions {
70 completionsKeyMap := DefaultKeyMap()
82 completionsKeyMap := DefaultKeyMap()
@@ -88,7 +100,7 @@ func New() Completions {
89 return &completionsCmp{
101 return &completionsCmp{
90 width: 0,
102 width: 0,
91− height: 0,
103+ height: maxCompletionsHeight,
93 query: "",
105 query: "",
94 keyMap: completionsKeyMap,
106 keyMap: completionsKeyMap,
@@ -107,8 +119,7 @@ func (c *completionsCmp) Init() tea.Cmd {
107func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
108 switch msg := msg.(type) {
120 switch msg := msg.(type) {
109 case tea.WindowSizeMsg:
121 case tea.WindowSizeMsg:
110− c.width = min(msg.Width-c.x, maxCompletionsWidth)
122+ c.wWidth, c.wHeight = msg.Width, msg.Height
111− c.height = min(msg.Height-c.y, 15)
112 return c, nil
123 return c, nil
113 case tea.KeyPressMsg:
124 case tea.KeyPressMsg:
@@ -129,7 +140,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
129 selectedItem := *s
140 selectedItem := *s
130 c.list.SetSelected(selectedItem.ID())
141 c.list.SetSelected(selectedItem.ID())
131 return c, util.CmdHandler(SelectCompletionMsg{
142 return c, util.CmdHandler(SelectCompletionMsg{
132− Value: selectedItem,
143+ Value: selectedItem.Value(),
133 Insert: true,
144 Insert: true,
135 case key.Matches(msg, c.keyMap.DownInsert):
146 case key.Matches(msg, c.keyMap.DownInsert):
@@ -140,7 +151,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
140 selectedItem := *s
151 selectedItem := *s
141 c.list.SetSelected(selectedItem.ID())
152 c.list.SetSelected(selectedItem.ID())
142 return c, util.CmdHandler(SelectCompletionMsg{
153 return c, util.CmdHandler(SelectCompletionMsg{
143− Value: selectedItem,
154+ Value: selectedItem.Value(),
144 Insert: true,
155 Insert: true,
146 case key.Matches(msg, c.keyMap.Select):
157 case key.Matches(msg, c.keyMap.Select):
@@ -151,18 +162,21 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
151 selectedItem := *s
162 selectedItem := *s
152 c.open = false // Close completions after selection
163 c.open = false // Close completions after selection
153 return c, util.CmdHandler(SelectCompletionMsg{
164 return c, util.CmdHandler(SelectCompletionMsg{
154− Value: selectedItem,
165+ Value: selectedItem.Value(),
156 case key.Matches(msg, c.keyMap.Cancel):
167 case key.Matches(msg, c.keyMap.Cancel):
157 return c, util.CmdHandler(CloseCompletionsMsg{})
168 return c, util.CmdHandler(CloseCompletionsMsg{})
170+ case RepositionCompletionsMsg:
171+ c.x, c.y = msg.X, msg.Y
159 case CloseCompletionsMsg:
173 case CloseCompletionsMsg:
160 c.open = false
174 c.open = false
161 return c, util.CmdHandler(CompletionsClosedMsg{})
175 return c, util.CmdHandler(CompletionsClosedMsg{})
162 case OpenCompletionsMsg:
176 case OpenCompletionsMsg:
163 c.open = true
177 c.open = true
164 c.query = ""
178 c.query = ""
165− c.x = msg.X
179+ c.x, c.xorig = msg.X, msg.X
166 c.y = msg.Y
180 c.y = msg.Y
167 items := []list.CompletionItem[any]{}
181 items := []list.CompletionItem[any]{}
168 t := styles.CurrentTheme()
182 t := styles.CurrentTheme()
@@ -174,10 +188,18 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175 items = append(items, item)
189 items = append(items, item)
191+ width := listWidth(items)
192+ if len(items) == 0 {
193+ width = listWidth(c.list.Items())
195+ if c.x+width >= c.wWidth {
196+ c.x = c.wWidth - width - 1
177− c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height
199+ c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height
178 return c, tea.Batch(
200 return c, tea.Batch(
179− c.list.SetSize(c.width, c.height),
180 c.list.SetItems(items),
201 c.list.SetItems(items),
202+ c.list.SetSize(c.width, c.height),
181 util.CmdHandler(CompletionsOpenedMsg{}),
203 util.CmdHandler(CompletionsOpenedMsg{}),
183 case FilterCompletionsMsg:
205 case FilterCompletionsMsg:
@@ -201,8 +223,11 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
201 c.query = msg.Query
223 c.query = msg.Query
202 var cmds []tea.Cmd
224 var cmds []tea.Cmd
203 cmds = append(cmds, c.list.Filter(msg.Query))
225 cmds = append(cmds, c.list.Filter(msg.Query))
204− itemsLen := len(c.list.Items())
226+ items := c.list.Items()
205− c.height = max(min(maxCompletionsHeight, itemsLen), 1)
227+ itemsLen := len(items)
229+ c.x, c.y = msg.X, msg.Y
206 cmds = append(cmds, c.list.SetSize(c.width, c.height))
231 cmds = append(cmds, c.list.SetSize(c.width, c.height))
207 if itemsLen == 0 {
232 if itemsLen == 0 {
208 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
233 cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{}))
@@ -215,21 +240,50 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
215 return c, nil
240 return c, nil
243+func (c *completionsCmp) adjustPosition() {
244+ items := c.list.Items()
245+ itemsLen := len(items)
246+ width := listWidth(items)
247+ c.lastWidth = c.width
248+ if c.x < 0 || width < c.lastWidth {
250+ } else if c.x+width >= c.wWidth {
251+ c.x = c.wWidth - width - 1
254+ c.height = max(min(maxCompletionsHeight, itemsLen), 1)
218// View implements Completions.
257// View implements Completions.
219func (c *completionsCmp) View() string {
258func (c *completionsCmp) View() string {
220 if !c.open || len(c.list.Items()) == 0 {
259 if !c.open || len(c.list.Items()) == 0 {
221 return ""
260 return ""
224− return c.style().Render(c.list.View())
227−func (c *completionsCmp) style() lipgloss.Style {
228 t := styles.CurrentTheme()
263 t := styles.CurrentTheme()
229− return t.S().Base.
264+ style := t.S().Base.
230 Width(c.width).
265 Width(c.width).
231 Height(c.height).
266 Height(c.height).
232 Background(t.BgSubtle)
267 Background(t.BgSubtle)
269+ return style.Render(c.list.View())
272+// listWidth returns the width of the last 10 items in the list, which is used
273+// to determine the width of the completions popup.
274+// Note this only works for [completionItemCmp] items.
275+func listWidth(items []list.CompletionItem[any]) int {
277+ if len(items) == 0 {
281+ for i := len(items) - 1; i >= 0 && i >= len(items)-10; i-- {
282+ itemWidth := lipgloss.Width(items[i].Text()) + 2 // +2 for padding
283+ width = max(width, itemWidth)
235func (c *completionsCmp) Open() bool {
289func (c *completionsCmp) Open() bool {