summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2021-05-14 11:43:32 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2021-05-14 11:44:44 +0900
commit3f75a8369f63f2bd6ac3686fc5d88f2bc128e610 (patch)
treefe7b9a042f9356a4169538c0571cfca79f7401bf
parent4cd621e877cb3a8e44b12ba3a7ce58709862922f (diff)
Replace RuneWidth to StringWidth to handle grapheme clusters
Fix #2482
-rw-r--r--go.mod2
-rw-r--r--src/options.go8
-rw-r--r--src/terminal.go58
-rw-r--r--src/tui/light.go31
-rw-r--r--src/tui/tcell.go82
-rw-r--r--src/util/util.go33
6 files changed, 108 insertions, 106 deletions
diff --git a/go.mod b/go.mod
index a5a3a6ba..a28d6f8e 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.12
github.com/mattn/go-shellwords v1.0.11
- github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/rivo/uniseg v0.2.0
github.com/saracen/walker v0.1.2
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
diff --git a/src/options.go b/src/options.go
index a136b859..dc40fdc7 100644
--- a/src/options.go
+++ b/src/options.go
@@ -1536,15 +1536,13 @@ func validateSign(sign string, signOptName string) error {
if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName)
}
- widthSum := 0
for _, r := range sign {
if !unicode.IsGraphic(r) {
return fmt.Errorf("invalid character in %v", signOptName)
}
- widthSum += runewidth.RuneWidth(r)
- if widthSum > 2 {
- return fmt.Errorf("%v display width should be up to 2", signOptName)
- }
+ }
+ if runewidth.StringWidth(sign) > 2 {
+ return fmt.Errorf("%v display width should be up to 2", signOptName)
}
return nil
}
diff --git a/src/terminal.go b/src/terminal.go
index 54f96675..aabeb07c 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -2,7 +2,6 @@ package fzf
import (
"bufio"
- "bytes"
"fmt"
"io/ioutil"
"os"
@@ -15,6 +14,9 @@ import (
"syscall"
"time"
+ "github.com/mattn/go-runewidth"
+ "github.com/rivo/uniseg"
+
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
@@ -673,11 +675,8 @@ func (t *Terminal) sortSelected() []selectedItem {
}
func (t *Terminal) displayWidth(runes []rune) int {
- l := 0
- for _, r := range runes {
- l += util.RuneWidth(r, l, t.tabstop)
- }
- return l
+ width, _ := util.RunesWidth(runes, 0, t.tabstop, 0)
+ return width
}
const (
@@ -1141,28 +1140,18 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
t.prevLines[i] = newLine
}
-func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) {
+func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters
- l := 0
- for idx, r := range runes {
- l += util.RuneWidth(r, l, t.tabstop)
- if l > width {
- return runes[:idx], len(runes) - idx
- }
+ width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
+ if overflowIdx >= 0 {
+ return runes[:overflowIdx], true
}
- return runes, 0
+ return runes, false
}
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
- l := 0
- for _, r := range runes {
- l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
- if l > limit {
- // Early exit
- return l
- }
- }
- return l
+ width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
+ return width
}
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
@@ -1362,9 +1351,9 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
- trimmedLen := 0
+ isTrimmed := false
if !t.previewOpts.wrap {
- trimmed, trimmedLen = t.trimRight(trimmed, maxWidth-t.pwindow.X())
+ trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width
@@ -1374,7 +1363,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
- return trimmedLen == 0 &&
+ return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
})
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
@@ -1430,16 +1419,21 @@ func (t *Terminal) printPreviewDelayed() {
}
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
- var strbuf bytes.Buffer
+ var strbuf strings.Builder
l := prefixWidth
- for _, r := range runes {
- w := util.RuneWidth(r, l, t.tabstop)
- l += w
- if r == '\t' {
+ gr := uniseg.NewGraphemes(string(runes))
+ for gr.Next() {
+ rs := gr.Runes()
+ str := string(rs)
+ var w int
+ if len(rs) == 1 && rs[0] == '\t' {
+ w = t.tabstop - l%t.tabstop
strbuf.WriteString(strings.Repeat(" ", w))
} else {
- strbuf.WriteRune(r)
+ w = runewidth.StringWidth(str)
+ strbuf.WriteString(str)
}
+ l += w
}
return strbuf.String(), l
}
diff --git a/src/tui/light.go b/src/tui/light.go
index 91b4c18e..d3e3faba 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -10,7 +10,8 @@ import (
"time"
"unicode/utf8"
- "github.com/junegunn/fzf/src/util"
+ "github.com/mattn/go-runewidth"
+ "github.com/rivo/uniseg"
"golang.org/x/term"
)
@@ -50,7 +51,7 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
}
bytes = bytes[sz:]
}
- r.queued += string(runes)
+ r.queued.WriteString(string(runes))
}
func (r *LightRenderer) csi(code string) {
@@ -58,9 +59,9 @@ func (r *LightRenderer) csi(code string) {
}
func (r *LightRenderer) flush() {
- if len(r.queued) > 0 {
- fmt.Fprint(os.Stderr, r.queued)
- r.queued = ""
+ if r.queued.Len() > 0 {
+ fmt.Fprint(os.Stderr, r.queued.String())
+ r.queued.Reset()
}
}
@@ -82,7 +83,7 @@ type LightRenderer struct {
escDelay int
fullscreen bool
upOneLine bool
- queued string
+ queued strings.Builder
y int
x int
maxHeightFunc func(int) int
@@ -889,20 +890,26 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
lines := []wrappedLine{}
width := 0
line := ""
- for _, r := range input {
- w := util.RuneWidth(r, prefixLength+width, 8)
- width += w
- str := string(r)
- if r == '\t' {
+ gr := uniseg.NewGraphemes(input)
+ for gr.Next() {
+ rs := gr.Runes()
+ str := string(rs)
+ var w int
+ if len(rs) == 1 && rs[0] == '\t' {
+ w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w)
+ } else {
+ w = runewidth.StringWidth(str)
}
+ width += w
+
if prefixLength+width <= max {
line += str
} else {
lines = append(lines, wrappedLine{string(line), width - w})
line = str
prefixLength = 0
- width = util.RuneWidth(r, prefixLength, 8)
+ width = w
}
}
lines = append(lines, wrappedLine{string(line), width})
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 938c1ba0..859e6709 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -5,7 +5,6 @@ package tui
import (
"os"
"time"
- "unicode/utf8"
"runtime"
@@ -13,6 +12,7 @@ import (
"github.com/gdamore/tcell/encoding"
"github.com/mattn/go-runewidth"
+ "github.com/rivo/uniseg"
)
func HasFullscreenRenderer() bool {
@@ -482,7 +482,6 @@ func (w *TcellWindow) Print(text string) {
}
func (w *TcellWindow) printString(text string, pair ColorPair) {
- t := text
lx := 0
a := pair.Attr()
@@ -496,33 +495,28 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
Dim(a&Attr(tcell.AttrDim) != 0)
}
- for {
- if len(t) == 0 {
- break
- }
- r, size := utf8.DecodeRuneInString(t)
- t = t[size:]
-
- if r < rune(' ') { // ignore control characters
- continue
- }
-
- if r == '\n' {
- w.lastY++
- lx = 0
- } else {
+ gr := uniseg.NewGraphemes(text)
+ for gr.Next() {
+ rs := gr.Runes()
- if r == '\u000D' { // skip carriage return
+ if len(rs) == 1 {
+ r := rs[0]
+ if r < rune(' ') { // ignore control characters
+ continue
+ } else if r == '\n' {
+ w.lastY++
+ lx = 0
+ continue
+ } else if r == '\u000D' { // skip carriage return
continue
}
-
- var xPos = w.left + w.lastX + lx
- var yPos = w.top + w.lastY
- if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
- _screen.SetContent(xPos, yPos, r, nil, style)
- }
- lx += runewidth.RuneWidth(r)
}
+ var xPos = w.left + w.lastX + lx
+ var yPos = w.top + w.lastY
+ if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
+ _screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
+ }
+ lx += runewidth.StringWidth(string(rs))
}
w.lastX += lx
}
@@ -549,30 +543,32 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Underline(a&Attr(tcell.AttrUnderline) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
- for _, r := range text {
- if r == '\n' {
+ gr := uniseg.NewGraphemes(text)
+ for gr.Next() {
+ rs := gr.Runes()
+ if len(rs) == 1 && rs[0] == '\n' {
w.lastY++
w.lastX = 0
lx = 0
- } else {
- var xPos = w.left + w.lastX + lx
-
- // word wrap:
- if xPos >= (w.left + w.width) {
- w.lastY++
- w.lastX = 0
- lx = 0
- xPos = w.left
- }
- var yPos = w.top + w.lastY
+ continue
+ }
- if yPos >= (w.top + w.height) {
- return FillSuspend
- }
+ // word wrap:
+ xPos := w.left + w.lastX + lx
+ if xPos >= (w.left + w.width) {
+ w.lastY++
+ w.lastX = 0
+ lx = 0
+ xPos = w.left
+ }
- _screen.SetContent(xPos, yPos, r, nil, style)
- lx += runewidth.RuneWidth(r)
+ yPos := w.top + w.lastY
+ if yPos >= (w.top + w.height) {
+ return FillSuspend
}
+
+ _screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
+ lx += runewidth.StringWidth(string(rs))
}
w.lastX += lx
if w.lastX == w.width {
diff --git a/src/util/util.go b/src/util/util.go
index 0aa1d804..59fb5708 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -7,22 +7,29 @@ import (
"github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth"
+ "github.com/rivo/uniseg"
)
-var _runeWidths = make(map[rune]int)
-
-// RuneWidth returns rune width
-func RuneWidth(r rune, prefixWidth int, tabstop int) int {
- if r == '\t' {
- return tabstop - prefixWidth%tabstop
- } else if w, found := _runeWidths[r]; found {
- return w
- } else if r == '\n' || r == '\r' {
- return 1
+// RunesWidth returns runes width
+func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
+ width := 0
+ gr := uniseg.NewGraphemes(string(runes))
+ idx := 0
+ for gr.Next() {
+ rs := gr.Runes()
+ var w int
+ if len(rs) == 1 && rs[0] == '\t' {
+ w = tabstop - (prefixWidth+width)%tabstop
+ } else {
+ w = runewidth.StringWidth(string(rs))
+ }
+ width += w
+ if limit > 0 && width > limit {
+ return width, idx
+ }
+ idx += len(rs)
}
- w := runewidth.RuneWidth(r)
- _runeWidths[r] = w
- return w
+ return width, -1
}
// Max returns the largest integer