Skip to content

Commit eb43184

Browse files
authored
Merge pull request #301 from clipperhouse/clipperhouse/displaywidth-experiment
Width performance
2 parents 64bc45f + bb166ce commit eb43184

File tree

9 files changed

+862
-39
lines changed

9 files changed

+862
-39
lines changed

benchstat.txt

Lines changed: 194 additions & 0 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/olekukonko/tablewriter
33
go 1.21
44

55
require (
6+
github.com/clipperhouse/displaywidth v0.3.1
67
github.com/clipperhouse/uax29/v2 v2.2.0
78
github.com/fatih/color v1.15.0
89
github.com/mattn/go-runewidth v0.0.19
@@ -12,6 +13,7 @@ require (
1213
)
1314

1415
require (
16+
github.com/clipperhouse/stringish v0.1.1 // indirect
1517
github.com/mattn/go-colorable v0.1.13 // indirect
1618
github.com/mattn/go-isatty v0.0.19 // indirect
1719
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
2+
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
3+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
4+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
15
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
26
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
37
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=

new.txt

Lines changed: 248 additions & 0 deletions
Large diffs are not rendered by default.

old.txt

Lines changed: 248 additions & 0 deletions
Large diffs are not rendered by default.

option.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,9 +647,9 @@ func WithEastAsian(enable bool) Option {
647647
// The runewidth.Condition object allows for more fine-grained control over how rune widths
648648
// are determined, beyond just toggling EastAsianWidth. This could include settings for
649649
// ambiguous width characters or other future properties of runewidth.Condition.
650-
func WithCondition(condition *runewidth.Condition) Option {
650+
func WithCondition(cond *runewidth.Condition) Option {
651651
return func(target *Table) {
652-
twwidth.SetCondition(condition)
652+
twwidth.SetCondition(cond)
653653
}
654654
}
655655

pkg/twwarp/wrap_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import (
1818

1919
"github.com/olekukonko/tablewriter/pkg/twwidth"
2020
"github.com/olekukonko/tablewriter/tw"
21-
22-
"github.com/mattn/go-runewidth"
2321
)
2422

2523
var (
@@ -59,7 +57,7 @@ func TestWrapOneLine(t *testing.T) {
5957
func TestUnicode(t *testing.T) {
6058
input := "Česká řeřicha"
6159
var wordsUnicode []string
62-
if runewidth.IsEastAsian() {
60+
if twwidth.IsEastAsian() {
6361
wordsUnicode, _ = WrapString(input, 14)
6462
} else {
6563
wordsUnicode, _ = WrapString(input, 13)
@@ -71,7 +69,7 @@ func TestUnicode(t *testing.T) {
7169
func TestDisplayWidth(t *testing.T) {
7270
input := "Česká řeřicha"
7371
want := 13
74-
if runewidth.IsEastAsian() {
72+
if twwidth.IsEastAsian() {
7573
want = 14
7674
}
7775
if n := twwidth.Width(input); n != want {

pkg/twwidth/width.go

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66
"strings"
77
"sync"
88

9+
"github.com/clipperhouse/displaywidth"
910
"github.com/mattn/go-runewidth"
1011
)
1112

12-
// condition holds the global runewidth configuration, including East Asian width settings.
13-
var condition *runewidth.Condition
13+
// globalOptions holds the global displaywidth configuration, including East Asian width settings.
14+
var globalOptions displaywidth.Options
1415

1516
// mu protects access to condition and widthCache for thread safety.
1617
var mu sync.Mutex
@@ -19,10 +20,21 @@ var mu sync.Mutex
1920
var ansi = Filter()
2021

2122
func init() {
22-
condition = runewidth.NewCondition()
23+
globalOptions = newOptions()
2324
widthCache = make(map[cacheKey]int)
2425
}
2526

27+
func newOptions() displaywidth.Options {
28+
// go-runewidth has default logic based on env variables and locale,
29+
// we want to keep that compatibility
30+
cond := runewidth.NewCondition()
31+
options := displaywidth.Options{
32+
EastAsianWidth: cond.EastAsianWidth,
33+
StrictEmojiNeutral: cond.StrictEmojiNeutral,
34+
}
35+
return options
36+
}
37+
2638
// cacheKey is used as a key for memoizing string width results in widthCache.
2739
type cacheKey struct {
2840
str string // Input string
@@ -60,26 +72,42 @@ func Filter() *regexp.Regexp {
6072
func SetEastAsian(enable bool) {
6173
mu.Lock()
6274
defer mu.Unlock()
63-
if condition.EastAsianWidth != enable {
64-
condition.EastAsianWidth = enable
75+
if globalOptions.EastAsianWidth != enable {
76+
globalOptions.EastAsianWidth = enable
6577
widthCache = make(map[cacheKey]int) // Clear cache on setting change
6678
}
6779
}
6880

69-
// SetCondition updates the global runewidth.Condition used for width calculations.
70-
// When the condition is changed, the width cache is cleared.
81+
// IsEastAsian returns the current East Asian width setting.
7182
// This function is thread-safe.
7283
//
7384
// Example:
7485
//
75-
// newCond := runewidth.NewCondition()
76-
// newCond.EastAsianWidth = true
77-
// twdw.SetCondition(newCond)
78-
func SetCondition(newCond *runewidth.Condition) {
86+
// if twdw.IsEastAsian() {
87+
// // Handle East Asian width characters
88+
// }
89+
func IsEastAsian() bool {
90+
mu.Lock()
91+
defer mu.Unlock()
92+
return globalOptions.EastAsianWidth
93+
}
94+
95+
// SetCondition updates the global runewidth.Condition used for width calculations.
96+
// This method is kept for backward compatibility. The condition is converted to
97+
// displaywidth.Options internally for better performance.
98+
func SetCondition(cond *runewidth.Condition) {
7999
mu.Lock()
80100
defer mu.Unlock()
81-
condition = newCond
82101
widthCache = make(map[cacheKey]int) // Clear cache on setting change
102+
globalOptions = conditionToOptions(cond)
103+
}
104+
105+
// Convert runewidth.Condition to displaywidth.Options
106+
func conditionToOptions(cond *runewidth.Condition) displaywidth.Options {
107+
return displaywidth.Options{
108+
EastAsianWidth: cond.EastAsianWidth,
109+
StrictEmojiNeutral: cond.StrictEmojiNeutral,
110+
}
83111
}
84112

85113
// Width calculates the visual width of a string, excluding ANSI escape sequences,
@@ -92,19 +120,18 @@ func SetCondition(newCond *runewidth.Condition) {
92120
// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10
93121
func Width(str string) int {
94122
mu.Lock()
95-
key := cacheKey{str: str, eastAsianWidth: condition.EastAsianWidth}
123+
key := cacheKey{str: str, eastAsianWidth: globalOptions.EastAsianWidth}
96124
if w, found := widthCache[key]; found {
97125
mu.Unlock()
98126
return w
99127
}
100128
mu.Unlock()
101129

102-
// Use a temporary condition to avoid holding the lock during calculation
103-
tempCond := runewidth.NewCondition()
104-
tempCond.EastAsianWidth = key.eastAsianWidth
130+
options := newOptions()
131+
options.EastAsianWidth = key.eastAsianWidth
105132

106133
stripped := ansi.ReplaceAllLiteralString(str, "")
107-
calculatedWidth := tempCond.StringWidth(stripped)
134+
calculatedWidth := options.String(stripped)
108135

109136
mu.Lock()
110137
widthCache[key] = calculatedWidth
@@ -122,14 +149,14 @@ func Width(str string) int {
122149
// width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10
123150
func WidthNoCache(str string) int {
124151
mu.Lock()
125-
currentEA := condition.EastAsianWidth
152+
currentEA := globalOptions.EastAsianWidth
126153
mu.Unlock()
127154

128-
tempCond := runewidth.NewCondition()
129-
tempCond.EastAsianWidth = currentEA
155+
options := newOptions()
156+
options.EastAsianWidth = currentEA
130157

131158
stripped := ansi.ReplaceAllLiteralString(str, "")
132-
return tempCond.StringWidth(stripped)
159+
return options.String(stripped)
133160
}
134161

135162
// Display calculates the visual width of a string, excluding ANSI escape sequences,
@@ -142,7 +169,8 @@ func WidthNoCache(str string) int {
142169
// cond := runewidth.NewCondition()
143170
// width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10
144171
func Display(cond *runewidth.Condition, str string) int {
145-
return cond.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
172+
options := conditionToOptions(cond)
173+
return options.String(ansi.ReplaceAllLiteralString(str, ""))
146174
}
147175

148176
// Truncate shortens a string to fit within a specified visual width, optionally
@@ -205,7 +233,7 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
205233

206234
// Capture the global EastAsianWidth setting once for consistent use
207235
mu.Lock()
208-
currentGlobalEastAsianWidth := condition.EastAsianWidth
236+
currentGlobalEastAsianWidth := globalOptions.EastAsianWidth
209237
mu.Unlock()
210238

211239
// Special case for EastAsian true: if only suffix fits, return suffix.
@@ -243,8 +271,8 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
243271
inAnsiSequence := false
244272
ansiWrittenToContent := false
245273

246-
localRunewidthCond := runewidth.NewCondition()
247-
localRunewidthCond.EastAsianWidth = currentGlobalEastAsianWidth
274+
options := newOptions()
275+
options.EastAsianWidth = currentGlobalEastAsianWidth
248276

249277
for _, r := range s {
250278
if r == '\x1b' {
@@ -278,7 +306,7 @@ func Truncate(s string, maxWidth int, suffix ...string) string {
278306
ansiSeqBuf.Reset()
279307
}
280308
} else { // Normal character
281-
runeDisplayWidth := localRunewidthCond.RuneWidth(r)
309+
runeDisplayWidth := options.Rune(r)
282310
if targetContentForIteration == 0 { // No budget for content at all
283311
break
284312
}

0 commit comments

Comments
 (0)