diff --git a/src/terminal.go b/src/terminal.go index e996dbfc..b997d60e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -3475,20 +3475,74 @@ func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit in func (t *Terminal) trimLeft(runes []rune, width int, ellipsisWidth int) ([]rune, int32) { width = max(0, width) var trimmed int32 - // Assume that each rune takes at least one column on screen - if len(runes) > width { - diff := len(runes) - width - trimmed = int32(diff) - runes = runes[diff:] + + str := string(runes) + runningSum := 0 + runningSumAdjusted := 0 + // We can't just subtract the width on each segment because there might be + // a tab character afterwards. For example, with the tabstop = 8: + // 1234____5678 + // 234_____5678 + // 34______5678 + // 4_______5678 + // ________5678 + // 5678 + // 678 + // 78 + // 8 + // We need to look ahead, but not to the end to avoid performance hit. + type queuedSegment struct { + rs []rune + w int + } + allQueue := []queuedSegment{} + queuedWidth := 0 + limit := width - ellipsisWidth + processQueue := func() { + for idx, item := range allQueue { + if runningSumAdjusted <= limit { + allQueue = allQueue[idx:] + return + } + runningSumAdjusted -= item.w + runes = runes[len(item.rs):] + trimmed += int32(len(item.rs)) + } + allQueue = []queuedSegment{} } - currentWidth := t.displayWidth(runes) + gr := uniseg.NewGraphemes(str) + queue := []queuedSegment{} + for gr.Next() { + s := gr.Str() + rs := gr.Runes() - for currentWidth > width-ellipsisWidth && len(runes) > 0 { - runes = runes[1:] - trimmed++ - currentWidth = t.displayWidthWithLimit(runes, ellipsisWidth, width) + var w int + if len(rs) == 1 && rs[0] == '\t' { + w = t.tabstop - runningSum%t.tabstop + } else { + w = util.StringWidth(string(rs)) + } + runningSum += w + runningSumAdjusted += w + queue = append(queue, queuedSegment{rs: rs, w: w}) + queuedWidth += w + if queuedWidth >= t.tabstop || s == "\t" { + queuedWidth = 0 + + if s == "\t" { + queue[len(queue)-1].w = t.tabstop + for idx := range queue[:len(queue)-1] { + queue[idx].w = 0 + } + } + allQueue = append(allQueue, queue...) + queue = []queuedSegment{} + processQueue() + } } + allQueue = append(allQueue, queue...) + processQueue() return runes, trimmed } @@ -3506,9 +3560,16 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat } charOffsets := matchOffsets if pos != nil { + runes := item.text.ToRunes() charOffsets = make([]Offset, len(*pos)) for idx, p := range *pos { - offset := Offset{int32(p), int32(p + 1)} + gr := uniseg.NewGraphemes(string(runes[p:])) + w := 1 + for gr.Next() { + w = len(gr.Runes()) + break + } + offset := Offset{int32(p), int32(p + w)} charOffsets[idx] = offset } sort.Sort(ByOrder(charOffsets)) @@ -3767,24 +3828,16 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat displayWidth = t.displayWidthWithLimit(runes, 0, adjustedMaxWidth) if !t.wrap && displayWidth > adjustedMaxWidth { maxe = util.Constrain(maxe+min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes)) - transformOffsets := func(diff int32, rightTrim bool) { - for idx, offset := range offs { - b, e := offset.offset[0], offset.offset[1] - el := int32(len(ellipsis)) - b += el - diff - e += el - diff - b = max(b, el) - if rightTrim { - e = min(e, int32(maxWidth-ellipsisWidth)) - } - offs[idx].offset[0] = b - offs[idx].offset[1] = max(b, e) + transformOffsets := func(diff int32) { + for idx := range offs { + offs[idx].offset[0] -= diff + offs[idx].offset[1] -= diff } } if t.hscroll { if fidx == 1 || fidx == 2 && t.keepRight && pos == nil { trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth) - transformOffsets(diff, false) + transformOffsets(diff - int32(len(ellipsis))) runes = append(ellipsis, trimmed...) } else if fidx == 0 || !t.overflow(runes[:maxe], maxWidth-ellipsisWidth) { // Stri.. @@ -3792,27 +3845,20 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat runes = append(runes, ellipsis...) } else { // Stri.. - rightTrim := false if t.overflow(runes[maxe:], ellipsisWidth) { runes = append(runes[:maxe], ellipsis...) - rightTrim = true } // ..ri.. var diff int32 runes, diff = t.trimLeft(runes, maxWidth, ellipsisWidth) // Transform offsets - transformOffsets(diff, rightTrim) + transformOffsets(diff - int32(len(ellipsis))) runes = append(ellipsis, runes...) } } else { runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth) runes = append(runes, ellipsis...) - - for idx, offset := range offs { - offs[idx].offset[0] = min(offset.offset[0], int32(maxWidth-len(ellipsis))) - offs[idx].offset[1] = min(offset.offset[1], int32(maxWidth)) - } } displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth) } diff --git a/src/util/util.go b/src/util/util.go index cf892398..fb94fb3c 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -18,8 +18,13 @@ func StringWidth(s string) int { // RunesWidth returns runes width func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { + return StringsWidth(string(runes), prefixWidth, tabstop, limit) +} + +// StringsWidth returns the width of the string +func StringsWidth(str string, prefixWidth int, tabstop int, limit int) (int, int) { width := 0 - gr := uniseg.NewGraphemes(string(runes)) + gr := uniseg.NewGraphemes(str) idx := 0 for gr.Next() { rs := gr.Runes() diff --git a/test/test_core.rb b/test/test_core.rb index db629ced..3f932eed 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -2175,4 +2175,13 @@ class TestCore < TestInteractive assert_equal 1, it.match_count end end + + def test_zero_width_characters + tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter + tmux.until do |lines| + assert_equal 981, lines.match_count + assert_match(/^> XX.*a̱500/, lines[-3]) + assert(lines.reverse.drop(5).all? { it.match?(/^ XX.*a̱500.*XX/) }) + end + end end