From 3aa3dae19a7db10b117d864affa3c51b2b1edc4c Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Mon, 31 Aug 2026 18:11:36 +0200 Subject: [PATCH] fix(tui): stabilize slash command description alignment Signed-off-by: Djordje Lukic --- pkg/tui/components/completion/completion.go | 16 ++++++---- .../components/completion/completion_test.go | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/pkg/tui/components/completion/completion.go b/pkg/tui/components/completion/completion.go index 1b92b40293..6aa536e79a 100644 --- a/pkg/tui/components/completion/completion.go +++ b/pkg/tui/components/completion/completion.go @@ -340,13 +340,7 @@ func (c *manager) View() string { visibleStart := c.scrollOffset visibleEnd := min(c.scrollOffset+maxItems, len(c.filteredItems)) - maxLabelLen := 0 - for i := visibleStart; i < visibleEnd; i++ { - labelLen := lipgloss.Width(c.filteredItems[i].Label) - if labelLen > maxLabelLen { - maxLabelLen = labelLen - } - } + maxLabelLen := c.labelColumnWidth() for i := visibleStart; i < visibleEnd; i++ { item := c.filteredItems[i] @@ -389,6 +383,14 @@ func (c *manager) View() string { return styles.CompletionBoxStyle.Render(content) } +func (c *manager) labelColumnWidth() int { + width := 0 + for _, item := range c.items { + width = max(width, lipgloss.Width(item.Label)) + } + return width +} + func (c *manager) GetLayers() []*lipgloss.Layer { if !c.visible { return nil diff --git a/pkg/tui/components/completion/completion_test.go b/pkg/tui/components/completion/completion_test.go index 9dd656e864..112eb53d4e 100644 --- a/pkg/tui/components/completion/completion_test.go +++ b/pkg/tui/components/completion/completion_test.go @@ -2,13 +2,45 @@ package completion import ( "reflect" + "strings" "testing" tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/ansi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestCompletionLabelColumnWidthIsStableWhileFiltering(t *testing.T) { + t.Parallel() + m := New().(*manager) + m.width = 80 + m.Update(OpenMsg{Items: []Item{ + {Label: "Short", Description: "Short description"}, + {Label: "Much Longer Command", Description: "Long description"}, + }}) + + fullLine := completionLine(t, m.View(), "Short") + fullDescriptionColumn := strings.Index(fullLine, "Short description") + require.Positive(t, fullDescriptionColumn) + + m.Update(QueryMsg{Query: "short"}) + filteredLine := completionLine(t, m.View(), "Short") + + assert.Equal(t, fullDescriptionColumn, strings.Index(filteredLine, "Short description")) +} + +func completionLine(t *testing.T, view, label string) string { + t.Helper() + for line := range strings.SplitSeq(ansi.Strip(view), "\n") { + if strings.Contains(line, label) { + return line + } + } + require.FailNow(t, "completion line not found", label) + return "" +} + func TestCompletionManagerStaysOpenWithNoResults(t *testing.T) { t.Parallel()