feat(tui): completions: dynamically adjust width based on items

Ayman Bagabas created

This will dynamically adjust the width of the completions popup based on
the width of the last 10 items in the list, ensuring that the popup
fits the content better and avoids unnecessary horizontal scrolling.

Change summary

internal/tui/components/completions/completions.go | 38 ++++++++++++---
1 file changed, 30 insertions(+), 8 deletions(-)

Detailed changes

internal/tui/components/completions/completions.go 🔗

@@ -102,7 +102,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.width = min(listWidth(c.list.Items()), maxCompletionsWidth)
 		c.height = min(msg.Height-c.y, 15)
 		return c, nil
 	case tea.KeyPressMsg:
@@ -168,10 +168,11 @@ 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.width = listWidth(msg.Completions)
 		c.height = max(min(c.height, 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,7 +196,9 @@ 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())
+		items := c.list.Items()
+		itemsLen := len(items)
+		c.width = listWidth(items)
 		c.height = max(min(maxCompletionsHeight, itemsLen), 1)
 		cmds = append(cmds, c.list.SetSize(c.width, c.height))
 		if itemsLen == 0 {
@@ -215,15 +218,34 @@ func (c *completionsCmp) View() string {
 		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 {