Skip to content

Commit 5b7acc4

Browse files
authored
fix: truncate overlay list rows to the item area (#504)
RenderOverlayList reserved a scrollbar column when the list overflows but never truncated rows, so a row filling the caller's full innerW budget (e.g. a multi-KB annotation value in the copy-field picker) exceeded the box content width, wrapped onto a second line, and pushed every row below out of sync with the per-row scrollbar glyphs. The component now owns the no-wider-than-innerW invariant: non-cursor rows truncate to the item area and the cursor row to its highlight width (which would otherwise wrap inside the Width block). Fixes the same latent overflow in the Object Explorer find overlay.
1 parent b53226e commit 5b7acc4

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

internal/ui/overlay_list.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,16 +207,22 @@ func RenderOverlayList(items []OverlayListItem, cfg OverlayListConfig, innerW in
207207
// so they stay readable against the box. CursorHighlightWidth
208208
// caps the highlight short of the item area; the remainder pads
209209
// with the surface background so the badge column stays put.
210+
// Text is truncated to the highlight width — anything longer
211+
// would wrap inside the Width(hlW) block, pushing every row
212+
// below out of sync with the scrollbar column.
210213
hlW := itemWidth
211214
if cfg.CursorHighlightWidth > 0 && cfg.CursorHighlightWidth < itemWidth {
212215
hlW = cfg.CursorHighlightWidth
213216
}
214-
row = OverlaySelectedStyle.Width(hlW).Render(itemPlainLine(it, cfg, hasActive))
217+
row = OverlaySelectedStyle.Width(hlW).Render(Truncate(itemPlainLine(it, cfg, hasActive), hlW))
215218
if pad := itemWidth - lipgloss.Width(row); pad > 0 {
216219
row += OverlayDimStyle.Render(strings.Repeat(" ", pad))
217220
}
218221
} else {
219-
line := OverlayNormalStyle.Render(itemStyledLine(it, cfg, hasActive))
222+
// Truncated to the item area for the same reason as the cursor
223+
// row: the component owns the no-wider-than-innerW invariant;
224+
// callers only best-effort their Name/Description budgets.
225+
line := OverlayNormalStyle.Render(Truncate(itemStyledLine(it, cfg, hasActive), itemWidth))
220226
// Only pad non-cursor rows when there's a trailing column
221227
// (badge or scrollbar) that needs a stable left edge —
222228
// padding rows without a reserve regresses the historical

internal/ui/overlay_list_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,64 @@ func TestRenderOverlayList(t *testing.T) {
388388
assert.Contains(t, out, "Stale")
389389
})
390390
}
391+
392+
// --- row width invariant ---
393+
394+
// Rows must never exceed innerW, no matter how long a caller-supplied
395+
// Name/Description is — an overlong row wraps inside the overlay box,
396+
// shifting every row below it and desyncing the per-row scrollbar
397+
// (observed with multi-KB annotation values in the copy-field picker).
398+
func TestRenderOverlayList_RowsNeverExceedInnerWidth(t *testing.T) {
399+
long := strings.Repeat("x", 300)
400+
mk := func(n int) []OverlayListItem {
401+
items := make([]OverlayListItem, n)
402+
for i := range items {
403+
items[i] = OverlayListItem{Name: long, Description: long}
404+
}
405+
return items
406+
}
407+
const innerW = 80
408+
409+
t.Run("overflowing list with scrollbar", func(t *testing.T) {
410+
cfg := OverlayListConfig{
411+
Title: "T",
412+
Cursor: 2,
413+
ShowDescription: true,
414+
MaxVisible: 5,
415+
}
416+
out := RenderOverlayList(mk(20), cfg, innerW)
417+
for i, line := range strings.Split(out, "\n") {
418+
require.LessOrEqualf(t, lipgloss.Width(line), innerW,
419+
"line %d wider than innerW", i)
420+
}
421+
})
422+
423+
t.Run("short list without scrollbar", func(t *testing.T) {
424+
cfg := OverlayListConfig{Cursor: 0, ShowDescription: true}
425+
out := RenderOverlayList(mk(3), cfg, innerW)
426+
for i, line := range strings.Split(out, "\n") {
427+
require.LessOrEqualf(t, lipgloss.Width(line), innerW,
428+
"line %d wider than innerW", i)
429+
}
430+
})
431+
432+
t.Run("cursor row with badge and multiselect", func(t *testing.T) {
433+
items := mk(12)
434+
for i := range items {
435+
items[i].Selected = i%2 == 0
436+
items[i].Badge = "##"
437+
}
438+
cfg := OverlayListConfig{
439+
Cursor: 4,
440+
MultiSelect: true,
441+
ShowDescription: true,
442+
MaxVisible: 6,
443+
BadgeWidth: 2,
444+
}
445+
out := RenderOverlayList(items, cfg, innerW)
446+
for i, line := range strings.Split(out, "\n") {
447+
require.LessOrEqualf(t, lipgloss.Width(line), innerW,
448+
"line %d wider than innerW", i)
449+
}
450+
})
451+
}

0 commit comments

Comments
 (0)