Merge pull request #1677 from cli/fix-issue-list-emoji-bug
Fix issue listing misalignment
This commit is contained in:
commit
b34fe8123a
4 changed files with 73 additions and 53 deletions
4
go.mod
4
go.mod
|
|
@ -14,15 +14,17 @@ require (
|
|||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.7
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/rivo/uniseg v0.1.0
|
||||
github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
||||
golang.org/x/text v0.3.3
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||
)
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -151,6 +151,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
|||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
|
|
|||
|
|
@ -1,80 +1,75 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"golang.org/x/text/width"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// DisplayWidth calculates what the rendered width of a string may be
|
||||
func DisplayWidth(s string) int {
|
||||
w := 0
|
||||
for _, r := range s {
|
||||
w += runeDisplayWidth(r)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
const (
|
||||
ellipsisWidth = 3
|
||||
minWidthForEllipsis = 5
|
||||
)
|
||||
|
||||
// DisplayWidth calculates what the rendered width of a string may be
|
||||
func DisplayWidth(s string) int {
|
||||
g := uniseg.NewGraphemes(s)
|
||||
w := 0
|
||||
for g.Next() {
|
||||
w += graphemeWidth(g)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Truncate shortens a string to fit the maximum display width
|
||||
func Truncate(max int, s string) string {
|
||||
func Truncate(maxWidth int, s string) string {
|
||||
w := DisplayWidth(s)
|
||||
if w <= max {
|
||||
if w <= maxWidth {
|
||||
return s
|
||||
}
|
||||
|
||||
useEllipsis := false
|
||||
if max >= minWidthForEllipsis {
|
||||
if maxWidth >= minWidthForEllipsis {
|
||||
useEllipsis = true
|
||||
max -= ellipsisWidth
|
||||
maxWidth -= ellipsisWidth
|
||||
}
|
||||
|
||||
cw := 0
|
||||
ri := 0
|
||||
for _, r := range s {
|
||||
rw := runeDisplayWidth(r)
|
||||
if cw+rw > max {
|
||||
g := uniseg.NewGraphemes(s)
|
||||
r := ""
|
||||
rWidth := 0
|
||||
for {
|
||||
g.Next()
|
||||
gWidth := graphemeWidth(g)
|
||||
|
||||
if rWidth+gWidth <= maxWidth {
|
||||
r += g.Str()
|
||||
rWidth += gWidth
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
cw += rw
|
||||
ri++
|
||||
}
|
||||
|
||||
res := string([]rune(s)[:ri])
|
||||
if useEllipsis {
|
||||
res += "..."
|
||||
}
|
||||
if cw < max {
|
||||
// compensate if truncating a wide character left an odd space
|
||||
res += " "
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var runeDisplayWidthOverrides = map[rune]int{
|
||||
'“': 1,
|
||||
'”': 1,
|
||||
'‘': 1,
|
||||
'’': 1,
|
||||
'–': 1, // en dash
|
||||
'—': 1, // em dash
|
||||
'→': 1,
|
||||
'…': 1,
|
||||
'•': 1, // bullet
|
||||
'·': 1, // middle dot
|
||||
}
|
||||
|
||||
func runeDisplayWidth(r rune) int {
|
||||
if w, ok := runeDisplayWidthOverrides[r]; ok {
|
||||
return w
|
||||
r += "..."
|
||||
}
|
||||
|
||||
switch width.LookupRune(r).Kind() {
|
||||
case width.EastAsianWide, width.EastAsianAmbiguous, width.EastAsianFullwidth:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
if rWidth < maxWidth {
|
||||
r += " "
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// graphemeWidth calculates what the rendered width of a grapheme may be
|
||||
func graphemeWidth(g *uniseg.Graphemes) int {
|
||||
// If grapheme spans one rune use that rune width
|
||||
// If grapheme spans multiple runes use the first non-zero rune width
|
||||
w := 0
|
||||
for _, r := range g.Runes() {
|
||||
w = runewidth.RuneWidth(r)
|
||||
if w > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,22 @@ func TestTruncate(t *testing.T) {
|
|||
},
|
||||
want: "a프로젝... ",
|
||||
},
|
||||
{
|
||||
name: "Emoji",
|
||||
args: args{
|
||||
max: 11,
|
||||
s: "💡💡💡💡💡💡💡💡💡💡💡💡",
|
||||
},
|
||||
want: "💡💡💡💡...",
|
||||
},
|
||||
{
|
||||
name: "Accented characters",
|
||||
args: args{
|
||||
max: 11,
|
||||
s: "é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́é́́",
|
||||
},
|
||||
want: "é́́é́́é́́é́́é́́é́́é́́é́́...",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -128,6 +144,11 @@ func TestDisplayWidth(t *testing.T) {
|
|||
text: `👍`,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "accent character",
|
||||
text: `é́́`,
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue