mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-13 03:11:08 +08:00
Replace --track=NTH with --id-nth for cross-reload item identity
Separate item identity from cursor tracking: - Add --id-nth=NTH to define item identity fields for cross-reload ops - --track reverts to a simple boolean flag - track-current action no longer accepts nth argument - With --multi, selections are preserved across reload-sync by matching identity keys in the reloaded list Close #4718 Close #4701 Close #4483 Close #4409 Close #3460 Close #2441
This commit is contained in:
+9
-9
@@ -3,16 +3,16 @@ CHANGELOG
|
|||||||
|
|
||||||
0.71.0
|
0.71.0
|
||||||
------
|
------
|
||||||
- Cross-reload tracking
|
- Cross-reload item identity with `--id-nth`
|
||||||
- `--track` now accepts an optional nth expression (`--track=NTH`) for field-based tracking across `reload`s
|
- Added `--id-nth=NTH` to define item identity fields for cross-reload operations
|
||||||
- `--track` without `NTH` retains the existing index-based tracking behavior (does not persist across reloads)
|
- When a `reload` is triggered with tracking enabled, fzf searches for the tracked item by its identity fields in the new list.
|
||||||
- `--track=..` tracks by the entire line across reloads
|
- `--track --id-nth ..` tracks by the entire line
|
||||||
- `--track=1` tracks by the first field across reloads
|
- `--track --id-nth 1` tracks by the first field
|
||||||
- When a `reload` is triggered, fzf searches for the tracked item by its nth field in the new list.
|
- `--track` without `--id-nth` retains the existing index-based tracking behavior
|
||||||
- The UI is temporarily blocked (prompt dimmed, input disabled) until the item is found or loading completes.
|
- The UI is temporarily blocked (prompt dimmed, input disabled) until the item is found or loading completes.
|
||||||
- Press `Escape` or `Ctrl-C` to cancel the blocked state without quitting
|
- Press `Escape` or `Ctrl-C` to cancel the blocked state without quitting
|
||||||
- Info line shows `+T*` / `+t*` while searching
|
- Info line shows `+T*` / `+t*` while searching
|
||||||
- `track-current` action now also accepts an optional nth argument: `track-current(1)`
|
- With `--multi`, selected items are preserved across `reload-sync` by matching their identity fields
|
||||||
- Performance improvements
|
- Performance improvements
|
||||||
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
|
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
|
||||||
```
|
```
|
||||||
|
|||||||
+31
-16
@@ -617,28 +617,18 @@ Disable multi-line display of items when using \fB\-\-read0\fR
|
|||||||
.B "\-\-raw"
|
.B "\-\-raw"
|
||||||
Enable raw mode where non-matching items are also displayed in a dimmed color.
|
Enable raw mode where non-matching items are also displayed in a dimmed color.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-track" "[=NTH]"
|
.BI "\-\-track"
|
||||||
Make fzf track the current selection when the result list is updated.
|
Make fzf track the current selection when the result list is updated.
|
||||||
This can be useful when browsing logs using fzf with sorting disabled. It is
|
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||||
not recommended to use this option with \fB\-\-tac\fR as the resulting behavior
|
not recommended to use this option with \fB\-\-tac\fR as the resulting behavior
|
||||||
can be confusing.
|
can be confusing.
|
||||||
|
|
||||||
When an nth expression is explicitly given (e.g. \fB\-\-track=..\fR or
|
When \fB\-\-id\-nth\fR is also set, fzf enables field\-based tracking across
|
||||||
\fB\-\-track=1\fR), fzf enables field\-based tracking across \fBreload\fRs.
|
\fBreload\fRs. See \fB\-\-id\-nth\fR for details.
|
||||||
On reload, fzf extracts the tracking key from the current item using the nth
|
|
||||||
expression and searches for a matching item in the reloaded list. While
|
|
||||||
searching, the UI is blocked (query input and cursor movement are disabled, and
|
|
||||||
the prompt is dimmed). With \fBreload\fR, the blocked state clears as soon as
|
|
||||||
the match is found in the stream. With \fBreload\-sync\fR, the blocked state
|
|
||||||
persists until the entire stream is complete. Press \fBEscape\fR or
|
|
||||||
\fBCtrl\-C\fR to cancel the blocked state without quitting fzf.
|
|
||||||
|
|
||||||
Without the nth expression, \fB\-\-track\fR uses index\-based tracking that
|
Without \fB\-\-id\-nth\fR, \fB\-\-track\fR uses index\-based tracking that
|
||||||
does not persist across reloads.
|
does not persist across reloads.
|
||||||
|
|
||||||
The info line shows \fB+T*\fR (or \fB+t*\fR for one\-off tracking) while
|
|
||||||
the search is in progress.
|
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Index\-based tracking (does not persist across reloads)
|
\fB# Index\-based tracking (does not persist across reloads)
|
||||||
@@ -646,10 +636,36 @@ e.g.
|
|||||||
fzf \-\-ansi \-\-track \-\-no\-sort \-\-layout=reverse\-list\fR
|
fzf \-\-ansi \-\-track \-\-no\-sort \-\-layout=reverse\-list\fR
|
||||||
|
|
||||||
\fB# Track by first field (e.g. pod name) across reloads
|
\fB# Track by first field (e.g. pod name) across reloads
|
||||||
kubectl get pods | fzf \-\-track=1 \-\-header\-lines=1 \\
|
kubectl get pods | fzf \-\-track \-\-id\-nth 1 \-\-header\-lines=1 \\
|
||||||
\-\-bind 'ctrl\-r:reload:kubectl get pods'\fR
|
\-\-bind 'ctrl\-r:reload:kubectl get pods'\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.BI "\-\-id\-nth=" "N[,..]"
|
||||||
|
Define item identity fields for cross\-reload operations. When set, fzf
|
||||||
|
uses the specified fields to identify items across \fBreload\fR and
|
||||||
|
\fBreload\-sync\fR.
|
||||||
|
|
||||||
|
With \fB\-\-track\fR, fzf extracts the tracking key from the current item
|
||||||
|
using the nth expression and searches for a matching item in the reloaded list.
|
||||||
|
While searching, the UI is blocked (query input and cursor movement are
|
||||||
|
disabled, and the prompt is dimmed). With \fBreload\fR, the blocked state
|
||||||
|
clears as soon as the match is found in the stream. With \fBreload\-sync\fR,
|
||||||
|
the blocked state persists until the entire stream is complete. Press
|
||||||
|
\fBEscape\fR or \fBCtrl\-C\fR to cancel the blocked state without quitting fzf.
|
||||||
|
|
||||||
|
The info line shows \fB+T*\fR (or \fB+t*\fR for one\-off tracking) while
|
||||||
|
the search is in progress.
|
||||||
|
|
||||||
|
With \fB\-\-multi\fR, selected items are preserved across \fBreload\-sync\fR
|
||||||
|
by matching their identity fields in the reloaded list.
|
||||||
|
|
||||||
|
.RS
|
||||||
|
e.g.
|
||||||
|
\fB# Track and preserve selections by pod name across reloads
|
||||||
|
kubectl get pods | fzf \-\-multi \-\-track \-\-id\-nth 1 \-\-header\-lines=1 \\
|
||||||
|
\-\-bind 'ctrl\-r:reload\-sync:kubectl get pods'\fR
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B "\-\-tac"
|
.B "\-\-tac"
|
||||||
Reverse the order of the input
|
Reverse the order of the input
|
||||||
|
|
||||||
@@ -2027,7 +2043,6 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
|
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
|
||||||
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
|
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
|
||||||
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
||||||
\fBtrack\-current(...)\fR (track the current item using the given nth expression as the tracking key)
|
|
||||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||||
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
||||||
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
||||||
|
|||||||
+13
-20
@@ -101,6 +101,7 @@ Usage: fzf [options]
|
|||||||
--no-multi-line Disable multi-line display of items when using --read0
|
--no-multi-line Disable multi-line display of items when using --read0
|
||||||
--raw Enable raw mode (show non-matching items)
|
--raw Enable raw mode (show non-matching items)
|
||||||
--track Track the current selection when the result is updated
|
--track Track the current selection when the result is updated
|
||||||
|
--id-nth=N[,..] Define item identity fields for cross-reload operations
|
||||||
--tac Reverse the order of the input
|
--tac Reverse the order of the input
|
||||||
--gap[=N] Render empty lines between each item
|
--gap[=N] Render empty lines between each item
|
||||||
--gap-line[=STR] Draw horizontal line on each gap using the string
|
--gap-line[=STR] Draw horizontal line on each gap using the string
|
||||||
@@ -594,7 +595,7 @@ type Options struct {
|
|||||||
Sort int
|
Sort int
|
||||||
Raw bool
|
Raw bool
|
||||||
Track trackOption
|
Track trackOption
|
||||||
TrackNth []Range
|
IdNth []Range
|
||||||
Tac bool
|
Tac bool
|
||||||
Tail int
|
Tail int
|
||||||
Criteria []criterion
|
Criteria []criterion
|
||||||
@@ -1631,7 +1632,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
argActionRegexp = regexp.MustCompile(
|
argActionRegexp = regexp.MustCompile(
|
||||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|with-nth|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger|track(?:-current)?)`)
|
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|with-nth|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
||||||
splitRegexp = regexp.MustCompile("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
@@ -1955,12 +1956,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
if _, _, err := parseKeyChords(actionArg, spec[0:offset]+" target required"); err != nil {
|
if _, _, err := parseKeyChords(actionArg, spec[0:offset]+" target required"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
case actTrackCurrent:
|
|
||||||
if len(actionArg) > 0 {
|
|
||||||
if _, err := splitNth(actionArg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case actChangePreviewWindow:
|
case actChangePreviewWindow:
|
||||||
opts := previewOpts{}
|
opts := previewOpts{}
|
||||||
for _, arg := range strings.Split(actionArg, "|") {
|
for _, arg := range strings.Split(actionArg, "|") {
|
||||||
@@ -2166,8 +2161,6 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actTrigger
|
return actTrigger
|
||||||
case "search":
|
case "search":
|
||||||
return actSearch
|
return actSearch
|
||||||
case "track", "track-current":
|
|
||||||
return actTrackCurrent
|
|
||||||
}
|
}
|
||||||
return actIgnore
|
return actIgnore
|
||||||
}
|
}
|
||||||
@@ -2818,18 +2811,18 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
opts.Raw = false
|
opts.Raw = false
|
||||||
case "--track":
|
case "--track":
|
||||||
opts.Track = trackEnabled
|
opts.Track = trackEnabled
|
||||||
if ok, str := optionalNextString(); ok {
|
|
||||||
nth, err := splitNth(str)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opts.TrackNth = nth
|
|
||||||
} else {
|
|
||||||
opts.TrackNth = nil
|
|
||||||
}
|
|
||||||
case "--no-track":
|
case "--no-track":
|
||||||
opts.Track = trackDisabled
|
opts.Track = trackDisabled
|
||||||
opts.TrackNth = nil
|
case "--id-nth":
|
||||||
|
str, err := nextString("nth expression required")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.IdNth, err = splitNth(str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "--no-id-nth":
|
||||||
|
opts.IdNth = nil
|
||||||
case "--tac":
|
case "--tac":
|
||||||
opts.Tac = true
|
opts.Tac = true
|
||||||
case "--no-tac":
|
case "--no-tac":
|
||||||
|
|||||||
+26
-12
@@ -314,11 +314,12 @@ type Terminal struct {
|
|||||||
sort bool
|
sort bool
|
||||||
toggleSort bool
|
toggleSort bool
|
||||||
track trackOption
|
track trackOption
|
||||||
trackNth []Range
|
idNth []Range
|
||||||
trackKey string
|
trackKey string
|
||||||
trackBlocked bool
|
trackBlocked bool
|
||||||
trackSync bool
|
trackSync bool
|
||||||
trackKeyCache map[int32]bool
|
trackKeyCache map[int32]bool
|
||||||
|
pendingSelections map[string]selectedItem
|
||||||
targetIndex int32
|
targetIndex int32
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
expect map[tui.Event]string
|
expect map[tui.Event]string
|
||||||
@@ -1049,7 +1050,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
sort: opts.Sort > 0,
|
sort: opts.Sort > 0,
|
||||||
toggleSort: opts.ToggleSort,
|
toggleSort: opts.ToggleSort,
|
||||||
track: opts.Track,
|
track: opts.Track,
|
||||||
trackNth: opts.TrackNth,
|
idNth: opts.IdNth,
|
||||||
targetIndex: minItem.Index(),
|
targetIndex: minItem.Index(),
|
||||||
delimiter: opts.Delimiter,
|
delimiter: opts.Delimiter,
|
||||||
expect: opts.Expect,
|
expect: opts.Expect,
|
||||||
@@ -1857,7 +1858,14 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
}
|
}
|
||||||
if t.revision != newRevision {
|
if t.revision != newRevision {
|
||||||
if !t.revision.compatible(newRevision) {
|
if !t.revision.compatible(newRevision) {
|
||||||
// Reloaded: clear selection
|
// Reloaded: capture selection keys for restoration, then clear
|
||||||
|
if len(t.idNth) > 0 && t.multi > 0 && len(t.selected) > 0 {
|
||||||
|
t.pendingSelections = make(map[string]selectedItem, len(t.selected))
|
||||||
|
for _, sel := range t.selected {
|
||||||
|
key := t.trackKeyFor(sel.item, t.idNth)
|
||||||
|
t.pendingSelections[key] = sel
|
||||||
|
}
|
||||||
|
}
|
||||||
t.selected = make(map[int32]selectedItem)
|
t.selected = make(map[int32]selectedItem)
|
||||||
t.clearNumLinesCache()
|
t.clearNumLinesCache()
|
||||||
} else {
|
} else {
|
||||||
@@ -1912,7 +1920,7 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
idx := item.Index()
|
idx := item.Index()
|
||||||
match, ok := t.trackKeyCache[idx]
|
match, ok := t.trackKeyCache[idx]
|
||||||
if !ok {
|
if !ok {
|
||||||
match = t.trackKeyFor(item, t.trackNth) == t.trackKey
|
match = t.trackKeyFor(item, t.idNth) == t.trackKey
|
||||||
t.trackKeyCache[idx] = match
|
t.trackKeyCache[idx] = match
|
||||||
}
|
}
|
||||||
if match {
|
if match {
|
||||||
@@ -1943,6 +1951,18 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
t.cy = count - min(count, t.maxItems()) + pos
|
t.cy = count - min(count, t.maxItems()) + pos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Restore selections by id-nth key after reload completes
|
||||||
|
if !t.reading && t.pendingSelections != nil {
|
||||||
|
for i := 0; i < t.merger.Length() && len(t.pendingSelections) > 0; i++ {
|
||||||
|
item := t.merger.Get(i).item
|
||||||
|
key := t.trackKeyFor(item, t.idNth)
|
||||||
|
if sel, found := t.pendingSelections[key]; found {
|
||||||
|
t.selected[item.Index()] = selectedItem{sel.at, item}
|
||||||
|
delete(t.pendingSelections, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.pendingSelections = nil
|
||||||
|
}
|
||||||
needActivation := false
|
needActivation := false
|
||||||
if !t.reading {
|
if !t.reading {
|
||||||
switch t.resultMerger.Length() {
|
switch t.resultMerger.Length() {
|
||||||
@@ -7104,12 +7124,6 @@ func (t *Terminal) Loop() error {
|
|||||||
if !t.track.Global() {
|
if !t.track.Global() {
|
||||||
t.track = trackCurrent(t.currentIndex())
|
t.track = trackCurrent(t.currentIndex())
|
||||||
}
|
}
|
||||||
// Parse optional nth argument: track-current(1) or track-current(1,3)
|
|
||||||
if len(a.a) > 0 {
|
|
||||||
if nth, err := splitNth(a.a); err == nil {
|
|
||||||
t.trackNth = nth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req(reqInfo)
|
req(reqInfo)
|
||||||
case actUntrackCurrent:
|
case actUntrackCurrent:
|
||||||
if t.track.Current() {
|
if t.track.Current() {
|
||||||
@@ -7469,9 +7483,9 @@ func (t *Terminal) Loop() error {
|
|||||||
t.reading = true
|
t.reading = true
|
||||||
|
|
||||||
// Capture tracking key before reload
|
// Capture tracking key before reload
|
||||||
if !t.track.Disabled() && len(t.trackNth) > 0 {
|
if !t.track.Disabled() && len(t.idNth) > 0 {
|
||||||
if item := t.currentItem(); item != nil {
|
if item := t.currentItem(); item != nil {
|
||||||
t.trackKey = t.trackKeyFor(item, t.trackNth)
|
t.trackKey = t.trackKeyFor(item, t.idNth)
|
||||||
t.trackKeyCache = make(map[int32]bool)
|
t.trackKeyCache = make(map[int32]bool)
|
||||||
t.trackBlocked = true
|
t.trackBlocked = true
|
||||||
t.trackSync = reloadSync
|
t.trackSync = reloadSync
|
||||||
|
|||||||
+39
-15
@@ -1660,8 +1660,8 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_track_nth_reload_whole_line
|
def test_track_nth_reload_whole_line
|
||||||
# --track=.. should track by entire line across reloads
|
# --track --id-nth .. should track by entire line across reloads
|
||||||
tmux.send_keys "seq 1000 | #{FZF} --track=.. --bind 'ctrl-r:reload:seq 1000 | sort -R'", :Enter
|
tmux.send_keys "seq 1000 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:seq 1000 | sort -R'", :Enter
|
||||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
|
||||||
# Move to item 555
|
# Move to item 555
|
||||||
@@ -1683,8 +1683,8 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_track_nth_reload_field
|
def test_track_nth_reload_field
|
||||||
# --track=1 should track by first field across reloads
|
# --track --id-nth 1 should track by first field across reloads
|
||||||
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --track=1 --bind 'ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --track --id-nth 1 --bind 'ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 3, lines.match_count
|
assert_equal 3, lines.match_count
|
||||||
assert_includes lines, '> 1 apple'
|
assert_includes lines, '> 1 apple'
|
||||||
@@ -1704,7 +1704,7 @@ class TestCore < TestInteractive
|
|||||||
|
|
||||||
def test_track_nth_reload_no_match
|
def test_track_nth_reload_no_match
|
||||||
# When tracked item is not found after reload, cursor stays at current position
|
# When tracked item is not found after reload, cursor stays at current position
|
||||||
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track=.. --bind 'ctrl-r:reload:printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
||||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||||
@@ -1721,7 +1721,7 @@ class TestCore < TestInteractive
|
|||||||
|
|
||||||
def test_track_nth_blocked_indicator
|
def test_track_nth_blocked_indicator
|
||||||
# +T* should appear during reload and disappear when match is found
|
# +T* should appear during reload and disappear when match is found
|
||||||
tmux.send_keys "seq 100 | #{FZF} --track=.. --bind 'ctrl-r:reload:sleep 1; seq 100 | sort -R'", :Enter
|
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; seq 100 | sort -R'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 100, lines.match_count
|
assert_equal 100, lines.match_count
|
||||||
assert_includes lines[-2], '+T'
|
assert_includes lines[-2], '+T'
|
||||||
@@ -1741,7 +1741,7 @@ class TestCore < TestInteractive
|
|||||||
|
|
||||||
def test_track_nth_abort_unblocks
|
def test_track_nth_abort_unblocks
|
||||||
# Escape during track-blocked state should unblock, not quit
|
# Escape during track-blocked state should unblock, not quit
|
||||||
tmux.send_keys "seq 100 | #{FZF} --track=.. --bind 'ctrl-r:reload:sleep 3; seq 100'", :Enter
|
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 3; seq 100'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 100, lines.match_count
|
assert_equal 100, lines.match_count
|
||||||
assert_includes lines[-2], '+T'
|
assert_includes lines[-2], '+T'
|
||||||
@@ -1763,7 +1763,7 @@ class TestCore < TestInteractive
|
|||||||
# With async reload, +T* should clear as soon as the match streams in,
|
# With async reload, +T* should clear as soon as the match streams in,
|
||||||
# even while loading is still in progress.
|
# even while loading is still in progress.
|
||||||
# sleep 1 first so +T* is observable, then the match arrives, then more items after a delay.
|
# sleep 1 first so +T* is observable, then the match arrives, then more items after a delay.
|
||||||
tmux.send_keys "seq 5 | #{FZF} --track=.. --bind 'ctrl-r:reload:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
tmux.send_keys "seq 5 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 5, lines.match_count
|
assert_equal 5, lines.match_count
|
||||||
assert_includes lines, '> 1'
|
assert_includes lines, '> 1'
|
||||||
@@ -1784,7 +1784,7 @@ class TestCore < TestInteractive
|
|||||||
def test_track_nth_reload_sync_blocks_until_complete
|
def test_track_nth_reload_sync_blocks_until_complete
|
||||||
# With reload-sync, +T* should stay until the entire stream is complete,
|
# With reload-sync, +T* should stay until the entire stream is complete,
|
||||||
# even though the match arrives early in the stream.
|
# even though the match arrives early in the stream.
|
||||||
tmux.send_keys "seq 5 | #{FZF} --track=.. --bind 'ctrl-r:reload-sync:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
tmux.send_keys "seq 5 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload-sync:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 5, lines.match_count
|
assert_equal 5, lines.match_count
|
||||||
assert_includes lines, '> 1'
|
assert_includes lines, '> 1'
|
||||||
@@ -1810,7 +1810,7 @@ class TestCore < TestInteractive
|
|||||||
|
|
||||||
def test_track_nth_toggle_track_unblocks
|
def test_track_nth_toggle_track_unblocks
|
||||||
# toggle-track during track-blocked state should unblock and disable tracking
|
# toggle-track during track-blocked state should unblock and disable tracking
|
||||||
tmux.send_keys "seq 100 | #{FZF} --track=.. --bind 'ctrl-r:reload:sleep 5; seq 100' --bind 'ctrl-t:toggle-track'", :Enter
|
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 5; seq 100' --bind 'ctrl-t:toggle-track'", :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 100, lines.match_count
|
assert_equal 100, lines.match_count
|
||||||
assert_includes lines[-2], '+T'
|
assert_includes lines[-2], '+T'
|
||||||
@@ -1830,7 +1830,7 @@ class TestCore < TestInteractive
|
|||||||
def test_track_nth_reload_async_no_match
|
def test_track_nth_reload_async_no_match
|
||||||
# With async reload, when tracked item is not found, cursor stays at
|
# With async reload, when tracked item is not found, cursor stays at
|
||||||
# current position after stream completes
|
# current position after stream completes
|
||||||
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track=.. --bind 'ctrl-r:reload:sleep 1; printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
||||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||||
@@ -1846,12 +1846,12 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_track_action_with_nth
|
def test_track_action_with_id_nth
|
||||||
# track(1) action should track by first field
|
# track-current with --id-nth should track by specified field
|
||||||
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --bind 'ctrl-t:track(1),ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --id-nth 1 --bind 'ctrl-t:track-current,ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
||||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
|
||||||
# Move to "2 banana" and activate field tracking
|
# Move to "2 banana" and activate tracking
|
||||||
tmux.send_keys :Up
|
tmux.send_keys :Up
|
||||||
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
||||||
tmux.send_keys 'C-t'
|
tmux.send_keys 'C-t'
|
||||||
@@ -1865,6 +1865,30 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_id_nth_preserve_multi_selection
|
||||||
|
# --id-nth with --multi should preserve selections across reload-sync
|
||||||
|
File.write(tempname, "1 apricot\n2 blueberry\n3 cranberry\n")
|
||||||
|
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{fzf("--multi --id-nth 1 --bind 'ctrl-r:reload-sync:cat #{tempname}'")}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
|
||||||
|
# Select first item (1 apple) and third item (3 cherry)
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.send_keys :Up, :Up, :Tab
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||||
|
|
||||||
|
# Reload — selections should be preserved by id-nth key
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 3, lines.match_count
|
||||||
|
assert_includes lines[-2], '(2)'
|
||||||
|
assert(lines.any? { |l| l.include?('apricot') })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Accept and verify the correct items were preserved
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal ['1 apricot', '3 cranberry'], fzf_output_lines
|
||||||
|
end
|
||||||
|
|
||||||
def test_one_and_zero
|
def test_one_and_zero
|
||||||
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||||
tmux.send_keys '1'
|
tmux.send_keys '1'
|
||||||
|
|||||||
Reference in New Issue
Block a user