pkg/util: Move the wcwidth utilities to a new package, and add more tests.

This addresses #944.
This commit is contained in:
Qi Xiao 2020-04-06 12:00:01 +01:00
parent f55bc315c9
commit 3037f6c8bf
13 changed files with 121 additions and 125 deletions

View File

@ -3,7 +3,7 @@ package cli
import (
"github.com/elves/elvish/pkg/cli/term"
"github.com/elves/elvish/pkg/ui"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// View model, calculated from State and used for rendering.
@ -112,7 +112,7 @@ func truncateToHeight(b *term.Buffer, maxHeight int) {
func styledWcswidth(t ui.Text) int {
w := 0
for _, seg := range t {
w += util.Wcswidth(seg.Text)
w += wcwidth.Of(seg.Text)
}
return w
}

View File

@ -1,6 +1,6 @@
package cli
import "github.com/elves/elvish/pkg/util"
import "github.com/elves/elvish/pkg/wcwidth"
// The number of lines the listing mode keeps between the current selected item
// and the top and bottom edges of the window, unless the available height is
@ -133,7 +133,7 @@ func maxWidth(items Items, padding, low, high int) int {
for i := low; i < high && i < n; i++ {
w := 0
for _, seg := range items.Show(i) {
w += util.Wcswidth(seg.Text)
w += wcwidth.Of(seg.Text)
}
if width < w {
width = w

View File

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// Cell is an indivisible unit on the screen. It is not necessarily 1 column
@ -23,7 +23,7 @@ type Pos struct {
func CellsWidth(cs []Cell) int {
w := 0
for _, c := range cs {
w += util.Wcswidth(c.Text)
w += wcwidth.Of(c.Text)
}
return w
}
@ -171,7 +171,7 @@ func (b *Buffer) TTYString() string {
lastStyle = cell.Style
}
sb.WriteString(cell.Text)
usedWidth += util.Wcswidth(cell.Text)
usedWidth += wcwidth.Of(cell.Text)
}
if lastStyle != "" {
sb.WriteString("\033[m")

View File

@ -4,7 +4,7 @@ import (
"strings"
"github.com/elves/elvish/pkg/ui"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// BufferBuilder supports building of Buffer.
@ -68,7 +68,7 @@ func (bb *BufferBuilder) appendLine() {
func (bb *BufferBuilder) appendCell(c Cell) {
n := len(bb.Lines)
bb.Lines[n-1] = append(bb.Lines[n-1], c)
bb.Col += util.Wcswidth(c.Text)
bb.Col += wcwidth.Of(c.Text)
}
// Newline starts a newline.
@ -104,7 +104,7 @@ func (bb *BufferBuilder) WriteRuneSGR(r rune, style string) *BufferBuilder {
c = Cell{"^" + string(r^0x40), style}
}
if bb.Col+util.Wcswidth(c.Text) > bb.Width {
if bb.Col+wcwidth.Of(c.Text) > bb.Width {
bb.Newline()
bb.appendCell(c)
} else {

View File

@ -5,7 +5,7 @@ import (
"os"
"github.com/elves/elvish/pkg/sys"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// Setup sets up the terminal so that it is suitable for the Reader and
@ -53,7 +53,7 @@ func setupVT(out *os.File) error {
LackEOL character. Otherwise, we are now in the next line and this is
a no-op. The LackEOL character remains.
*/
s += fmt.Sprintf("\033[?7h%s%*s\r \r", lackEOL, width-util.Wcwidth(lackEOLRune), "")
s += fmt.Sprintf("\033[?7h%s%*s\r \r", lackEOL, width-wcwidth.OfRune(lackEOLRune), "")
/*
Turn off autowrap.

View File

@ -5,7 +5,7 @@ import (
"github.com/elves/elvish/pkg/cli/term"
"github.com/elves/elvish/pkg/ui"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// TextView is a Widget for displaying text, with support for vertical
@ -68,7 +68,7 @@ func (w *textView) Render(width, height int) *term.Buffer {
if i > first {
bb.Newline()
}
bb.Write(util.TrimWcwidth(lines[i], textWidth))
bb.Write(wcwidth.Trim(lines[i], textWidth))
}
buf := bb.Buffer()

View File

@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// Context is a range of text in a source code. It is typically used for
@ -92,7 +92,7 @@ func (c *Context) ShowCompact(sourceIndent string) string {
}
desc := c.Name + ", " + c.lineRange() + " "
// Extra indent so that following lines line up with the first line.
descIndent := strings.Repeat(" ", util.Wcswidth(desc))
descIndent := strings.Repeat(" ", wcwidth.Of(desc))
return desc + c.relevantSource(sourceIndent+descIndent)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/elves/elvish/pkg/parse/parseutil"
"github.com/elves/elvish/pkg/ui"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
//elvdoc:fn binding-table
@ -333,8 +334,8 @@ func moveDotUp(buffer string, dot int) int {
}
prevEOL := sol - 1
prevSOL := util.FindLastSOL(buffer[:prevEOL])
width := util.Wcswidth(buffer[sol:dot])
return prevSOL + len(util.TrimWcwidth(buffer[prevSOL:prevEOL], width))
width := wcwidth.Of(buffer[sol:dot])
return prevSOL + len(wcwidth.Trim(buffer[prevSOL:prevEOL], width))
}
//elvdoc:fn move-dot-down
@ -351,8 +352,8 @@ func moveDotDown(buffer string, dot int) int {
nextSOL := eol + 1
nextEOL := util.FindFirstEOL(buffer[nextSOL:]) + nextSOL
sol := util.FindLastSOL(buffer[:dot])
width := util.Wcswidth(buffer[sol:dot])
return nextSOL + len(util.TrimWcwidth(buffer[nextSOL:nextEOL], width))
width := wcwidth.Of(buffer[sol:dot])
return nextSOL + len(wcwidth.Trim(buffer[nextSOL:nextEOL], width))
}
// TODO(xiaq): Document the concepts of words, small words and alnum words.

View File

@ -10,7 +10,7 @@ import (
"unicode/utf8"
"github.com/elves/elvish/pkg/eval/vals"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// String operations.
@ -260,8 +260,8 @@ func init() {
"chr": chr,
"base": base,
"wcswidth": util.Wcswidth,
"-override-wcwidth": util.OverrideWcwidth,
"wcswidth": wcwidth.Of,
"-override-wcwidth": wcwidth.Override,
"has-prefix": strings.HasPrefix,
"has-suffix": strings.HasSuffix,

View File

@ -6,7 +6,7 @@ import (
"strconv"
"github.com/elves/elvish/pkg/eval/vals"
"github.com/elves/elvish/pkg/util"
"github.com/elves/elvish/pkg/wcwidth"
)
// Text contains of a list of styled Segments.
@ -177,10 +177,10 @@ func (t Text) SplitByRune(r rune) []Text {
func (t Text) TrimWcwidth(wmax int) Text {
var newt Text
for _, seg := range t {
w := util.Wcswidth(seg.Text)
w := wcwidth.Of(seg.Text)
if w >= wmax {
newt = append(newt,
&Segment{seg.Style, util.TrimWcwidth(seg.Text, wmax)})
&Segment{seg.Style, wcwidth.Trim(seg.Text, wmax)})
break
}
wmax -= w

View File

@ -1,76 +0,0 @@
package util
import (
"testing"
)
var wcwidthTests = []struct {
in rune
wanted int
}{
{'\u0301', 0}, // Combining acute accent
{'a', 1},
{'Ω', 1},
{'好', 2},
{'か', 2},
}
func TestWcwidth(t *testing.T) {
for _, tt := range wcwidthTests {
out := Wcwidth(tt.in)
if out != tt.wanted {
t.Errorf("wcwidth(%q) => %v, want %v", tt.in, out, tt.wanted)
}
}
}
func TestOverrideWcwidth(t *testing.T) {
r := '❱'
oldw := Wcwidth(r)
w := oldw + 1
OverrideWcwidth(r, w)
if Wcwidth(r) != w {
t.Errorf("Wcwidth(%q) != %d after OverrideWcwidth", r, w)
}
UnoverrideWcwidth(r)
if Wcwidth(r) != oldw {
t.Errorf("Wcwidth(%q) != %d after UnoverrideWcwidth", r, oldw)
}
}
func TestTrimWcwidth(t *testing.T) {
if TrimWcwidth("abc", 2) != "ab" {
t.Errorf("TrimWcwidth #1 fails")
}
if TrimWcwidth("你好", 3) != "你" {
t.Errorf("TrimWcwidth #2 fails")
}
}
func TestForceWcwidth(t *testing.T) {
for i, c := range []struct {
s string
w int
want string
}{
// Triming
{"abc", 2, "ab"},
{"你好", 2, "你"},
// Padding
{"abc", 4, "abc "},
{"你好", 5, "你好 "},
// Trimming and Padding
{"你好", 3, "你 "},
} {
if got := ForceWcwidth(c.s, c.w); got != c.want {
t.Errorf("ForceWcwidth #%d fails", i)
}
}
}
func TestTrimEachLineWcwidth(t *testing.T) {
if TrimEachLineWcwidth("abcdefg\n你好", 3) != "abc\n你" {
t.Errorf("TestTrimEachLineWcwidth fails")
}
}

View File

@ -1,4 +1,6 @@
package util
// Package wcwidth provides utilities for determining the column width of
// characters when displayed on the terminal.
package wcwidth
import (
"sort"
@ -65,8 +67,8 @@ func isCombining(r rune) bool {
return i < n && r >= combining[i][0]
}
// Wcwidth returns the width of a rune when displayed on the terminal.
func Wcwidth(r rune) int {
// OfRune returns the column width of a rune.
func OfRune(r rune) int {
if w, ok := wcwidthOverride[r]; ok {
return w
}
@ -95,34 +97,33 @@ func Wcwidth(r rune) int {
return 1
}
// OverrideWcwidth overrides the wcwidth of a rune to be a specific non-negative
// value. OverrideWcwidth panics if w < 0.
func OverrideWcwidth(r rune, w int) {
// Override overrides the column width of a rune to be a specific non-negative
// value. It panics if w < 0.
func Override(r rune, w int) {
if w < 0 {
panic("negative width")
}
wcwidthOverride[r] = w
}
// UnoverrideWcwidth removes the override of a rune.
func UnoverrideWcwidth(r rune) {
// Unoverride removes the column width override of a rune.
func Unoverride(r rune) {
delete(wcwidthOverride, r)
}
// Wcswidth returns the width of a string when displayed on the terminal,
// assuming no soft line breaks.
func Wcswidth(s string) (w int) {
// Of returns the column width of a string, assuming no soft line breaks.
func Of(s string) (w int) {
for _, r := range s {
w += Wcwidth(r)
w += OfRune(r)
}
return
}
// TrimWcwidth trims the string s so that it has a width of at most wmax.
func TrimWcwidth(s string, wmax int) string {
// Trim trims the string s so that it has a column width of at most wmax.
func Trim(s string, wmax int) string {
w := 0
for i, r := range s {
w += Wcwidth(r)
w += OfRune(r)
if w > wmax {
return s[:i]
}
@ -130,12 +131,11 @@ func TrimWcwidth(s string, wmax int) string {
return s
}
// ForceWcwidth forces the string s to the given display width by trimming and
// padding.
func ForceWcwidth(s string, width int) string {
// Force forces the string s to the given column width by trimming and padding.
func Force(s string, width int) string {
w := 0
for i, r := range s {
w0 := Wcwidth(r)
w0 := OfRune(r)
w += w0
if w > width {
w -= w0
@ -146,12 +146,12 @@ func ForceWcwidth(s string, width int) string {
return s + strings.Repeat(" ", width-w)
}
// TrimEachLineWcwidth trims each line of s so that it is no wider than the
// specified width.
func TrimEachLineWcwidth(s string, width int) string {
// TrimEachLine trims each line of s so that it is no wider than the specified
// width.
func TrimEachLine(s string, width int) string {
lines := strings.Split(s, "\n")
for i := range lines {
lines[i] = TrimWcwidth(lines[i], width)
lines[i] = Trim(lines[i], width)
}
return strings.Join(lines, "\n")
}

View File

@ -0,0 +1,71 @@
package wcwidth
import (
"testing"
"github.com/elves/elvish/pkg/tt"
)
var Args = tt.Args
func TestOf(t *testing.T) {
tt.Test(t, tt.Fn("Of", Of), tt.Table{
Args("\u0301").Rets(0), // Combining acute accent
Args("a").Rets(1),
Args("Ω").Rets(1),
Args("好").Rets(2),
Args("か").Rets(2),
Args("abc").Rets(3),
Args("你好").Rets(4),
})
}
func TestOverride(t *testing.T) {
r := '❱'
oldw := OfRune(r)
w := oldw + 1
Override(r, w)
if OfRune(r) != w {
t.Errorf("Wcwidth(%q) != %d after OverrideWcwidth", r, w)
}
Unoverride(r)
if OfRune(r) != oldw {
t.Errorf("Wcwidth(%q) != %d after UnoverrideWcwidth", r, oldw)
}
}
func TestTrim(t *testing.T) {
tt.Test(t, tt.Fn("Trim", Trim), tt.Table{
Args("abc", 1).Rets("a"),
Args("abc", 2).Rets("ab"),
Args("abc", 3).Rets("abc"),
Args("abc", 4).Rets("abc"),
Args("你好", 1).Rets(""),
Args("你好", 2).Rets("你"),
Args("你好", 3).Rets("你"),
Args("你好", 4).Rets("你好"),
Args("你好", 5).Rets("你好"),
})
}
func TestForce(t *testing.T) {
tt.Test(t, tt.Fn("Force", Force), tt.Table{
// Triming
Args("abc", 2).Rets("ab"),
Args("你好", 2).Rets("你"),
// Padding
Args("abc", 4).Rets("abc "),
Args("你好", 5).Rets("你好 "),
// Trimming and Padding
Args("你好", 3).Rets("你 "),
})
}
func TestTrimEachLine(t *testing.T) {
tt.Test(t, tt.Fn("TrimEachLine", TrimEachLine), tt.Table{
Args("abcdefg\n你好", 3).Rets("abc\n你"),
})
}