Add a truncation function that puts the ellipsis in the middle

TruncateWithEllipsis cuts off the end of a string, which is the wrong
end for a path: what distinguishes two paths is often the last segment,
and it is the one thing the reader wants to see. Keep both ends and put
the ellipsis between them.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-09-07 19:01:55 +02:00
parent 3c4d3920a3
commit 2135e928db
2 changed files with 75 additions and 0 deletions

View file

@ -199,6 +199,49 @@ func TruncateWithEllipsis(str string, limit int) string {
return truncatedStr + "…"
}
// TruncateWithEllipsisInMiddle returns a string, truncated to a certain width,
// with an ellipsis in the middle. Use it where the end of the string is as
// informative as its beginning, e.g. for paths.
func TruncateWithEllipsisInMiddle(str string, limit int) string {
if StringWidth(str) <= limit {
return str
}
if limit <= 2 {
return strings.Repeat(".", limit)
}
clusters := []string{}
widths := []int{}
graphemes := uniseg.NewGraphemes(str)
for graphemes.Next() {
clusters = append(clusters, graphemes.Str())
widths = append(widths, graphemes.Width())
}
// One column goes to the ellipsis; the rest is split between the two ends,
// with the odd one going to the front.
remaining := limit - 1
frontLimit := (remaining + 1) / 2
front := 0
frontWidth := 0
for front < len(clusters) && frontWidth+widths[front] <= frontLimit {
frontWidth += widths[front]
front++
}
// Whatever the front didn't use, e.g. because a wide grapheme didn't fit
// into it, is available to the back.
back := len(clusters)
backWidth := 0
for back > front && backWidth+widths[back-1] <= remaining-frontWidth {
backWidth += widths[back-1]
back--
}
return strings.Join(clusters[:front], "") + "…" + strings.Join(clusters[back:], "")
}
func SafeTruncate(str string, limit int) string {
if len(str) > limit {
return str[0:limit]

View file

@ -162,6 +162,38 @@ func TestTruncateWithEllipsis(t *testing.T) {
}
}
func TestTruncateWithEllipsisInMiddle(t *testing.T) {
type scenario struct {
str string
limit int
expected string
}
scenarios := []scenario{
{"hello world !", 0, ""},
{"hello world !", 1, "."},
{"hello world !", 2, ".."},
{"hello world !", 3, "h…!"},
{"hello world !", 4, "he…!"},
{"hello world !", 5, "he… !"},
{"hello world !", 12, "hello …rld !"},
{"hello world !", 13, "hello world !"},
{"hello world !", 14, "hello world !"},
// A wide grapheme that doesn't fit into the front leaves its column to
// the back
{"大大大大", 5, "大…大"},
{"大大大大", 7, "大…大大"},
{"大大大大", 8, "大大大大"},
{"大大大大", 2, ".."},
{"大大大大", 1, "."},
{"大大大大", 0, ""},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, TruncateWithEllipsisInMiddle(s.str, s.limit))
}
}
func TestRenderDisplayStrings(t *testing.T) {
type scenario struct {
input [][]string