Compare commits

..
1 Commits
Author SHA1 Message Date
Junegunn Choi 961793cf39 Fix --gap-line cutting a grapheme cluster
CodeQL / Analyze (go) (push) Waiting to run
build / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
RepeatToFill filled the remaining width rune by rune, so a cluster
could be split at the right edge. Iterate grapheme clusters instead.

Fix #4920
2026-09-13 19:31:36 +09:00
3 changed files with 34 additions and 7 deletions
+4
View File
@@ -1,6 +1,10 @@
CHANGELOG CHANGELOG
========= =========
0.74.5
------
- Fixed `--gap-line` cutting a grapheme cluster when filling the last cells of the line (#4920)
0.74.4 0.74.4
------ ------
- Fixed an escape sequence split across reads being parsed as a fragment, which leaked the rest into the query (#4899) - Fixed an escape sequence split across reads being parsed as a fragment, which leaked the rest into the query (#4899)
+9 -7
View File
@@ -105,16 +105,18 @@ func RepeatToFill(str string, length int, limit int) string {
rest := limit % length rest := limit % length
output := strings.Repeat(str, times) output := strings.Repeat(str, times)
if rest > 0 { if rest > 0 {
for _, r := range str { // Iterate over grapheme clusters so that we don't cut a cluster in half
rest -= uniseg.StringWidth(string(r)) end := 0
if rest < 0 { graphemes := uniseg.NewGraphemes(str)
break for rest > 0 && graphemes.Next() {
} width := graphemes.Width()
output += string(r) if width > rest {
if rest == 0 {
break break
} }
rest -= width
_, end = graphemes.Positions()
} }
output += str[:end]
} }
return output return output
} }
+21
View File
@@ -109,6 +109,27 @@ func TestRepeatToFill(t *testing.T) {
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] { if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2]) t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
} }
// Should not cut a grapheme cluster in half
for _, test := range []struct {
str string
limit int
expected string
}{
{"a\u0301b", 1, "a\u0301"},
{"a\u0301b", 3, "a\u0301ba\u0301"},
{"a\u0301b", 4, "a\u0301ba\u0301b"},
{"a\u4e00", 2, "a"},
{"a\u4e00", 4, "a\u4e00a"},
{"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 1, "-"},
{"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 2, "-"},
{"-\U0001f468\u200d\U0001f469\u200d\U0001f467", 4, "-\U0001f468\u200d\U0001f469\u200d\U0001f467-"},
} {
actual := RepeatToFill(test.str, StringWidth(test.str), test.limit)
if actual != test.expected {
t.Errorf("Expected: %q, actual: %q", test.expected, actual)
}
}
} }
func TestStringWidth(t *testing.T) { func TestStringWidth(t *testing.T) {