Add --ellipsis=.. option

Close #2432

Also see
- #1769
- https://github.com/junegunn/fzf/pull/1844#issuecomment-586663660
This commit is contained in:
Junegunn Choi
2022-03-29 21:35:36 +09:00
parent b88eb72ac2
commit ef67a45702
8 changed files with 97 additions and 22 deletions

View File

@@ -70,6 +70,7 @@ const usage = `usage: fzf [options]
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
Display
--ansi Enable processing of ANSI color codes
@@ -235,6 +236,7 @@ type Options struct {
Header []string
HeaderLines int
HeaderFirst bool
Ellipsis string
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
@@ -298,6 +300,7 @@ func defaultOptions() *Options {
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
Ellipsis: "..",
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
@@ -1280,6 +1283,7 @@ func parseOptions(opts *Options, allArgs []string) {
validateJumpLabels := false
validatePointer := false
validateMarker := false
validateEllipsis := false
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
switch arg {
@@ -1465,6 +1469,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--ellipsis":
opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
validateEllipsis = true
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
@@ -1562,6 +1569,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--ellipsis="); match {
opts.Ellipsis = value
validateEllipsis = true
} else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match {
@@ -1624,6 +1634,14 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit(err.Error())
}
}
if validateEllipsis {
for _, r := range opts.Ellipsis {
if !unicode.IsGraphic(r) {
errorExit("invalid character in ellipsis")
}
}
}
}
func validateSign(sign string, signOptName string) error {

View File

@@ -50,7 +50,6 @@ var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string
const ellipsis string = ".."
const clearCode string = "\x1b[2J"
func init() {
@@ -145,6 +144,7 @@ type Terminal struct {
headerLines int
header []string
header0 []string
ellipsis string
ansi bool
tabstop int
margin [4]sizeSpec
@@ -541,6 +541,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
headerLines: opts.HeaderLines,
header: header,
header0: header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
@@ -1261,47 +1262,54 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth {
transformOffsets := func(diff int32) {
transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
el := int32(len(ellipsis))
b += el - diff
e += el - diff
b = util.Max32(b, el)
if rightTrim {
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
}
if t.hscroll {
if t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(text, maxWidth-2)
transformOffsets(diff)
text = append([]rune(ellipsis), trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-2) {
trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
transformOffsets(diff, false)
text = append(ellipsis, trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
// Stri..
text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune(ellipsis)...)
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, ellipsis...)
} else {
// Stri..
if t.overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune(ellipsis)...)
rightTrim := false
if t.overflow(text[maxe:], ellipsisWidth) {
text = append(text[:maxe], ellipsis...)
rightTrim = true
}
// ..ri..
var diff int32
text, diff = t.trimLeft(text, maxWidth-2)
text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
// Transform offsets
transformOffsets(diff)
text = append([]rune(ellipsis), text...)
transformOffsets(diff, rightTrim)
text = append(ellipsis, text...)
}
} else {
text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune(ellipsis)...)
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, ellipsis...)
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
}

View File

@@ -34,6 +34,23 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
return width, -1
}
// Truncate returns the truncated runes and its width
func Truncate(input string, limit int) ([]rune, int) {
runes := []rune{}
width := 0
gr := uniseg.NewGraphemes(input)
for gr.Next() {
rs := gr.Runes()
w := runewidth.StringWidth(string(rs))
if width+w > limit {
return runes, width
}
width += w
runes = append(runes, rs...)
}
return runes, width
}
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {

View File

@@ -54,3 +54,13 @@ func TestRunesWidth(t *testing.T) {
}
}
}
func TestTruncate(t *testing.T) {
truncated, width := Truncate("가나다라마", 7)
if string(truncated) != "가나다" {
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
}
if width != 6 {
t.Errorf("Expected: 6, actual: %d", width)
}
}