From 1c7534f00966edca7c44054af199ca27aca0a80c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 29 Mar 2023 20:36:09 +0900 Subject: [PATCH] Add --track option to track the current selection Close #3186 Related #1890 --- CHANGELOG.md | 7 +++++++ man/man1/fzf.1 | 10 ++++++++++ src/merger.go | 17 ++++++++++++++++- src/options.go | 7 +++++++ src/terminal.go | 18 ++++++++++++++++++ test/test_go.rb | 23 +++++++++++++++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1bf3e..c776f41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ CHANGELOG 0.39.0 ------ +- Added `--track` option that makes fzf track the current selection when the + result list is updated. This can be useful when browsing logs using fzf with + sorting disabled. + ```sh + git log --oneline --graph --color=always | nl | + fzf --ansi --track --no-sort --layout=reverse-list + ``` - If you use `--listen` option without a port number fzf will automatically allocate an available port and export it as `$FZF_PORT` environment variable. diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a538a91..7d5ea73 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -92,6 +92,16 @@ interface rather than a "fuzzy finder". You can later enable the search using .B "+s, --no-sort" Do not sort the result .TP +.B "--track" +Make fzf track the current selection when the result list is updated. +This can be useful when browsing logs using fzf with sorting disabled. + +.RS +e.g. + \fBgit log --oneline --graph --color=always | nl | + fzf --ansi --track --no-sort --layout=reverse-list\fR +.RE +.TP .B "--tac" Reverse the order of the input diff --git a/src/merger.go b/src/merger.go index 8e6a884..cdf00ac 100644 --- a/src/merger.go +++ b/src/merger.go @@ -17,6 +17,7 @@ type Merger struct { tac bool final bool count int + pass bool } // PassMerger returns a new Merger that simply returns the items in the @@ -26,7 +27,8 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger { pattern: nil, chunks: chunks, tac: tac, - count: 0} + count: 0, + pass: true} for _, chunk := range *mg.chunks { mg.count += chunk.count @@ -58,6 +60,19 @@ func (mg *Merger) Length() int { return mg.count } +// FindIndex returns the index of the item with the given item index +func (mg *Merger) FindIndex(itemIndex int32) int { + if mg.pass { + return int(itemIndex) + } + for i := 0; i < mg.count; i++ { + if mg.Get(i).item.Index() == itemIndex { + return i + } + } + return -1 +} + // Get returns the pointer to the Result object indexed by the given integer func (mg *Merger) Get(idx int) Result { if mg.chunks != nil { diff --git a/src/options.go b/src/options.go index 9f953bd..36ca771 100644 --- a/src/options.go +++ b/src/options.go @@ -33,6 +33,7 @@ const usage = `usage: fzf [options] field index expressions -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result + --track Track the current selection when the result is updated --tac Reverse the order of the input --disabled Do not perform search --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply @@ -266,6 +267,7 @@ type Options struct { WithNth []Range Delimiter Delimiter Sort int + Track bool Tac bool Criteria []criterion Multi int @@ -338,6 +340,7 @@ func defaultOptions() *Options { WithNth: make([]Range, 0), Delimiter: Delimiter{}, Sort: 1000, + Track: false, Tac: false, Criteria: []criterion{byScore, byLength}, Multi: 0, @@ -1562,6 +1565,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sort = optionalNumeric(allArgs, &i, 1) case "+s", "--no-sort": opts.Sort = 0 + case "--track": + opts.Track = true + case "--no-track": + opts.Track = false case "--tac": opts.Tac = true case "--no-tac": diff --git a/src/terminal.go b/src/terminal.go index 468b90f..57ff4f5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -183,6 +183,7 @@ type Terminal struct { multi int sort bool toggleSort bool + track bool delimiter Delimiter expect map[tui.Event]string keymap map[tui.Event][]*action @@ -599,6 +600,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, + track: opts.Track, delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, @@ -904,6 +906,10 @@ func (t *Terminal) UpdateProgress(progress float32) { // UpdateList updates Merger to display the list func (t *Terminal) UpdateList(merger *Merger, reset bool) { t.mutex.Lock() + var prevIndex int32 = -1 + if !reset && t.track && t.merger.Length() > 0 { + prevIndex = t.merger.Get(t.cy).item.Index() + } t.progress = 100 t.merger = merger if reset { @@ -914,6 +920,18 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) { t.triggerLoad = false t.eventChan <- tui.Load.AsEvent() } + if prevIndex >= 0 { + pos := t.cy - t.offset + count := t.merger.Length() + i := t.merger.FindIndex(prevIndex) + if i >= 0 { + t.cy = i + t.offset = t.cy - pos + } else if t.cy > count { + // Try to keep the vertical position when the list shrinks + t.cy = count - util.Min(count, t.maxItems()) + pos + } + } t.mutex.Unlock() t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqList, nil) diff --git a/test/test_go.rb b/test/test_go.rb index 8da6cb5..1556c73 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2679,6 +2679,29 @@ class TestGoFZF < TestBase OUTPUT tmux.until { assert_block(expected, _1) } end + + def test_track + tmux.send_keys "seq 1000 | #{FZF} --query 555 --track", :Enter + tmux.until do |lines| + assert_equal 1, lines.match_count + assert_includes lines, '> 555' + end + tmux.send_keys :BSpace + index = tmux.until do |lines| + assert_equal 28, lines.match_count + assert_includes lines, '> 555' + end.index('> 555') + tmux.send_keys :BSpace + tmux.until do |lines| + assert_equal 271, lines.match_count + assert_equal '> 555', lines[index] + end + tmux.send_keys :BSpace + tmux.until do |lines| + assert_equal 1000, lines.match_count + assert_equal '> 555', lines[index] + end + end end module TestShell