From ef67a45702c01ff93e0ea99a51594c8160f66cc1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 29 Mar 2022 21:35:36 +0900 Subject: [PATCH] Add --ellipsis=.. option Close #2432 Also see - #1769 - https://github.com/junegunn/fzf/pull/1844#issuecomment-586663660 --- CHANGELOG.md | 13 ++++++++++++ man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 5 ++++- src/options.go | 18 ++++++++++++++++ src/terminal.go | 48 +++++++++++++++++++++++++------------------ src/util/util.go | 17 +++++++++++++++ src/util/util_test.go | 10 +++++++++ test/test_go.rb | 6 ++++++ 8 files changed, 97 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbcbea9..616d438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +0.30.0 +------ +- Added `--ellipsis` option. You can take advantage of it to make fzf + effectively search non-visible parts of the item. + ```sh + # Search against hidden line numbers on the far right + nl /usr/share/dict/words | + awk '{printf "%s%1000s\n", $2, $1}' | + fzf --nth=-1 --no-hscroll --ellipsis='' | + awk '{print $2}' + ``` +- Increased TTY buffer limit (#2748) + 0.29.0 ------ - Added `change-preview(...)` action to change the `--preview` command diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 2601d5b..d34edb4 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf-tmux 1 "Dec 2021" "fzf 0.29.0" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Mar 2022" "fzf 0.30.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4920dba..d5b3539 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Mar 2022" "fzf 0.30.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -302,6 +302,9 @@ lines that follow. .TP .B "--header-first" Print header before the prompt line +.TP +.BI "--ellipsis=" "STR" +Ellipsis to show when line is truncated (default: '..') .SS Display .TP .B "--ansi" diff --git a/src/options.go b/src/options.go index b3bcdea..cc77143 100644 --- a/src/options.go +++ b/src/options.go @@ -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 { diff --git a/src/terminal.go b/src/terminal.go index e4823ad..9ea0c1a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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)) } } diff --git a/src/util/util.go b/src/util/util.go index c3995bf..a1c37f7 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -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 { diff --git a/src/util/util_test.go b/src/util/util_test.go index 45a5a2d..20bdb92 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -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) + } +} diff --git a/test/test_go.rb b/test/test_go.rb index 95759bf..bd95060 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2208,6 +2208,12 @@ class TestGoFZF < TestBase tmux.send_keys 'a' end end + + def test_ellipsis + tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.until { |lines| assert_match(/^> SNIPSNIP.*SNIPSNIP$/, lines[-3]) } + end end module TestShell