- A symlink in one root was skipped when its target was an ancestor of
another root, hiding files reachable only through it
- Fix and tests by @mabunemeh
Close#4944
- `masked +=` per token copied the whole prefix on every append
- ~1 MB POST to --listen blocked the server for ~30 s
- switch to strings.Builder
Reported by Zachariah Mithani (@zachariah-mithani, AploSec).
- --sync, --tac, --bench disable streaming filter, and the result was sorted
- Matcher.scan already checks pattern.sortable
Problem analyzed and patch submitted by @NavpreetSSidhu
Fix#4940
Co-authored-by: NavpreetSSidhu <116211004+NavpreetSSidhu@users.noreply.github.com>
- Same rule as change-with-nth and transform-with-nth
- Empty: default
- '..': all fields
- Non-empty invalid: ignored
- Previously, change-nth() and empty transform-nth output searched
all fields, and transform-nth could not restore default
- Partly reverts af0014ab. Use change-nth(..) to unset default
- Find current value in bound list and apply next valid,
non-current one
- Keep rotating bound list as a position hint, so repeated
values in the list still reach every position
- Treat empty --nth and '..' as the same value
- Ignore invalid nth values, also in transform-nth, as with-nth
does. Previously, they restored default --nth
- Previously, first value of rotated list was applied blindly
- Key press spent on value already applied, e.g. first value
equal to initial --nth
- Stuck on change-nth printed by transform (new action on
each run), as it always applied first value
- Add nextValue helper for reuse by change-layout
- Rotation of '|'-separated values applied to transform and
bg-transform variants, where a.a is the command template
- Output '1|2' turned the binding into '2|1', run as a shell
command on the next key press
- Output was also cut at '|', breaking --with-nth templates
like '{2}|{1}'
- Split and rotate only for change-nth and change-with-nth
* fix(nushell): adapt the completer to the 0.116 contract
Nushell 0.116 runs an external completer on a background worker
without the terminal unless the registered closure reaches a
command carrying the @interactive attribute. Interactivity is
seen through the closure to the command it calls, so wrap the
completion logic in an @interactive entry point and register a
closure that ends with the call to it.
Read the named `place` input instead of the deprecated
positional `spans`; $place.command carries the same token list.
An @interactive completer cannot decline with null (null reads
as a dismissed picker), so request the fallback explicitly when
the `**` trigger is absent, keeping plain file completion
working. The previous external completer is still called when
the trigger is absent.
The script now requires Nushell 0.116+, since @interactive is a
parse error on older versions.
Fixes#4932
* fix(nushell): retry the previous completer with the legacy spans input
The fallback to the previously configured external completer always
handed it the new `place` input. A completer that still declares the
legacy `spans` parameter errors on a record, so the fallback silently
produced no completions for pre-0.116 completers (e.g. older Carapace
installs).
Try `place` first, then retry with the token list, which is what the
old positional bridge handed such completers.
* docs(nushell): note the 0.116 requirement and load order
Requires Nushell 0.116.1+, where a completion can take the terminal
through the @interactive attribute, and the wrapper captures the
external completer present at load time.
FIFO path was predictable (${TMPDIR:-/tmp}/fzf-complete-fifo-$$) and
_fzf_feed_fifo ignored rm/mkfifo failures, so on a shared /tmp another
user could pre-create the path (symlink or FIFO) and have the victim's
redirect follow it.
Allocate the FIFO inside a per-invocation mktemp -d directory, check
mkfifo status, and clean up the directory instead of the bare path.
Reported-by: sb24CK (https://github.com/sb24CK)
Attached option form (e.g. -l50) captured size via ${arg:2} with no
validation, unlike the separate-argument form. For -l/-u the value
reached $((...)) arithmetic, where a[$(cmd)] runs cmd during option
parsing, before any tmux check. Same value was also expanded unquoted
into `tmux popup ... $opt`, allowing tmux flag injection.
Apply the separate-form regex to the attached form too. Rejects the
array-subscript payload and space-bearing geometry values that enabled
flag injection.
Reported-by: sb24CK (https://github.com/sb24CK)
- Use new-pane -A on tmux 3.8+ to keep floating pane above zoomed pane
- Detect -A from new-pane usage; keep unzoom fallback for 3.7
Fix#4927Fixjunegunn/fzf.vim#1625
Co-authored-by: Jere Viikari <jere.viikari@gmail.com>
- Indexes restart after reload, so same-index item was not seen as new
- With --id-nth, compare by key. No event if focused item keeps its key.
- Skip focus check while track is blocked
- Document that binding reload to focus can loop
Close#4930
The parser decided how many bytes a key used before checking them, so a
key that arrived in the same read was lost or misread.
- Recognize F1-F8 and modified Delete, PageUp and PageDown when more input
follows them, as with a paste or tmux send-keys. They required the
sequence to be the whole buffer.
- After \e[ and a digit, do not consume a byte that cannot continue the
sequence. ALT-[, 2 and Enter lost the Enter.
- Report Home and End for \e[7, \e[4 and \e[8 only before ~, $, ^ or @.
ALT-[, 4 and x fired End and lost the x.
- Look for the end of a mouse report within its frame. A later M swallowed
the keys before it, and repeated \e[< took quadratic time.
fzf consumed only the part of a sequence it recognized, and the rest was
typed into the query. CTRL-A sent as \e[97;5u became "97;5u", and CTRL-F
sent as \e[70;5u fired Home before typing ";5u". A BEL-terminated OSC
reply also aborted fzf, because the BEL that followed the typed payload
was read as CTRL-G.
Frame CSI and SS3 by their parameter and final byte ranges, OSC, DCS and
APC by their terminator. Drop the whole frame when the parser does not
recognize it, or recognizes only a prefix of it, and never consume past
it: the parser could count a key typed after it. rxvt ends keys with $,
as in \e[7$ and \e[23$, so $ after plain digits is a final byte. Elsewhere
it stays intermediate, as in DECRPM replies.
Take the second-chance read only when the sequence at the start of the
buffer is the only one and is unfinished. Otherwise it blocked until the
next key, holding back what followed, CTRL-G included. That read has no
time limit, so what it returns may be the next key, typed any time later.
It is never dropped with the frame.
Telling a terminal's sequence from an ALT key is the hard part, so these
keep what the parser made of them:
- An unterminated sequence, since that is how ALT-[, ALT-O, ALT-], ALT-P
and ALT-_ arrive
- A CSI or SS3 of four bytes or fewer. It could be an ALT key and typed
text, and rxvt sends keys of that size fzf does not know, such as \e[3^
- A string sequence whose payload does not start like a reply: digits and
';' for OSC, '>|', '!|', '[01]$r' or '[01]+r' for DCS, 'G' and a key
for APC
- SOS and PM, which nothing sends
Within a string, an ESC ends it and introduces a sequence of its own, and
only OSC ends with BEL. The read loop does not wait for a string
terminator: that held ALT-], ALT-P and ALT-_ for ESCDELAY and let typed
bytes pile up until they looked like a sequence.
- Selections were appended to the command line as-is, so a path with
spaces became several arguments and syntax in a file name was left
unquoted for the parser
- Read the selection with --print0 and serialize each path with `to nuon`,
so each one stays a single string literal
- Skip the insertion entirely when nothing was selected
The custom CTRL-T command is now executed by the script in the current
shell session, instead of being called by fzf through a subshell. This
ensures that the value of $dir can't be altered by user initialization
scripts, and is also a little faster.
Fix#4498
- progress was set to 100 by every UpdateList, partial results included,
and never reset when a search started
- So a GET could report a new query, the previous result set, and
progress 100, and a client had no way to tell it was stale
- Now cleared where the search is requested and set to 100 only on the
final result. Query and the reset happen in the same locked section
that dumpStatus locks, so query == X with progress 100 means the
matches belong to X
Fix#4903
- extraLines() used borderLines(), which reports zero for BorderInline
- Divider drawn for an inline section still takes a line, and
resizeWindows() reserves it via addInline()
- So adaptive height came up one line short per inline section and the
list scrolled
- Affects --header-lines-border, --header-border and --footer-border
alike. All three set came up three lines short
- Without a list border the shape normalizes to BorderTop or BorderBottom,
which report a line, hence --no-list-border looked like a fix
seq 10 | fzf --height=~100% --list-border \
--header-lines=1 --header-lines-border=inline
Fix#4904
The window fzf was started from stays visible, and can be used while fzf
runs, which a popup inside Vim cannot offer.
Only where the pane is not modal, so tmux 3.7 or above, or Zellij 0.44 or
above with fzf 0.71.0 or above. A tmux popup from 3.3 to 3.6 cannot be left,
and below 3.3 fzf goes through the fzf-tmux script, whose options are
spelled differently. Both keep the window inside Vim. An explicit popup
layout is unaffected and still works from 3.3.
fzf checks tmux before Zellij, so tmux wins when both are set.
g:fzf_layout still wins, so anyone who set it sees no change.
* Run fzf asynchronously in the Vim plugin
Popup mode held the fzf process with system(), which froze Vim until fzf
exited. fzf in a popup draws in a pane of its own, so that process only
waits for it and does not need a window. Hold it with a job instead and
Vim keeps processing its event loop, which is what a live preview needs.
Nothing is displayed for the job. Falls back to the blocking path when
the job cannot start, so the sink still runs and temp files are removed.
- job_start() sets $TERM=dumb and the popup inherits the environment, so
fzf dropped to its 16-color scheme. Restore it via 'env', or in the
command itself before 8.0.902, when 'env' was added
- Fullscreen now uses a terminal buffer in a new tab on Vim too. use_term
lacked parentheses, so && bound tighter than || and the layout test was
dead on Neovim, which already behaved this way
- fzf#run returns an empty list in these modes. Callers use sink,
sinklist or exit, and the vader specs now wait for completion
- Append --no-tmux only when the spec asks for a Vim window, so --popup
in $FZF_DEFAULT_OPTS survives a spec with no layout option
Accept popup as a synonym of the tmux layout key, matching --popup being
the name of --tmux. popup wins when both are given.
s:tmux_enabled():
- Accept $ZELLIJ, which --popup covers as well
- Parse tmux -V with matchstr and compare with s:compare_versions. The
old string comparison against 'tmux 1.7' misreads 10.0
- Drop the fzf-tmux requirement on tmux 3.3 or above, where --tmux needs
no script. Removing the script silently disabled popups entirely
- Resolve the script where it is used, and anchor the legacy test to ^-
so a --tmux value containing a dash, as in 90%,60%,border-native, is
not mistaken for a legacy flag
* Turn g:loaded_fzf into a version marker
Callers cannot detect plugin-side behavior. fzf#exec() reports the version
of the binary, not of this plugin, and everything else here is script-local.
fzf.vim needs to know whether fzf#run is asynchronous in popup mode before
offering a key binding that depends on it.
The value was only ever read through the exists() guard, so raising it from
1 breaks nothing.
Ask the terminal whether the mode is already on (DECRQM) and put it back
that way on exit. Forcing it off broke pasting in shells that run fzf
from a line editor widget, which enable the mode only when the editor
starts. Terminals that do not answer fall back to disabling it.
- Startup queries go out in one write, cursor position last. Every
terminal answers DSR, so its reply bounds the wait: a paste reply
still missing by then means the terminal does not know the query.
- Bound the first read with select(2). Terminals that never answer
escape sequences, such as FreeBSD virtual terminals, blocked startup
until a key was pressed, and that keystroke was then discarded.
Fix#4887Fix#2860Fix#976
chafa ends each row of a Kitty Unicode placeholder image with CUB + IND
instead of a newline, so that an image drawn at a column offset survives
ONLCR, which would rewrite the newline as CR NL and pull the cursor back
to column 0. Both sequences were dropped, so the image arrived as a single
line and was re-wrapped, shifting every row after the first by the width
of the wrap sign. Without wrapping it was truncated to its first row.
The column is tracked across the breaks and restored with padding, so an
image indented by the preview command keeps its indent.
Fix#4885
--border-label of a tmux floating pane was stored in the pane title and
read back by pane-border-format as '#{pane_title}', so any program in the
pane could replace the border text by setting the title. On Zellij the
name was only set when the pane was created. Either way the built-in
label actions could not touch it, as fzf draws no border of its own
there.
- tmux: hold the label in pane-scoped @fzf-border-label. Set the option
even without a label, so a later change-border-label has somewhere to
write. Drop select-pane -T; the pane title is left to the user
- Zellij: run 'zellij action rename-pane' on label change. 'zellij run'
has no option to set the environment of the new pane, so the command
exports the target itself
- change-border-label and transform-border-label now update the native
border, through the setter nativeLabelSetter picks by multiplexer
- The pane is named in a __FZF_INTERNAL_ variable that runProxy withholds
from the environment replay, so the same name in the outer environment
cannot redirect the update to another pane
For two ASCII characters, rows 0 and 1 of the score matrix collapse to
scalar running state, so Phase 2 and Phase 3 fuse into one pass with no
score arrays. withPos stores the two rows for the backtrace. Up to 1.4x
on two-char queries, the most common multi-char length.
Verify both fast paths against the general algorithm with exhaustive
(every short string over a class-complete alphabet) and fuzz tests,
runnable via the new make fuzz target.
Precompute OR of all sort keys; a byte position that is zero across every
key contributes a no-op pass. Default two-criteria setup leaves the low 32
bits zero, so 4 of 8 passes are skipped without a histogram scan. 2x on the
sort, 3-8% on high-match queries. No regression when all bytes are used.
Decision must not depend on threshold chain resolution
(computePreviewSize consults noSeparatorLine); separator is hidden only
when every spec in the chain displays a 'next' preview window with a
border line facing the input section. Otherwise err on the side of
showing it.
- Without active previewer, preview position is resolved from first spec
alone, so 'next' in a threshold alternative does not create input
window and cannot hide the separator
- Empty preview command is undecidable; preview(...) action can display
ad-hoc window at 'next' position at any time
- change-preview-window relayouts or repaints when separator visibility
changes even if the active spec is unchanged, without dropping preview
window forced by preview(...) action
Visible input border suppressed separator even when it draws no line
toward list section (bottom in default layout, top in reverse, vertical).
Suppress only when input border has a line on the list side; otherwise
fall through to adjacent section check.
- Requires header window to have content before treating it as facing
section; hasHeaderWindow returns true with empty header when input
border is visible
- Extract inputBorderFacesList and resolveInputBorderShape shared by
options and terminal
- Add tests for separator suppression rules
- Applies when adjacent section (header, header lines, or list) draws
border line facing input section
# No separator below header border
fzf --style full --input-border none --header foo
# Separator shown; nothing separates input from list
fzf --style full --input-border none --header foo --no-header-border
- Evaluated dynamically so change-header/toggle-header update it
- Inline header is drawn inside list border and not adjacent to input;
decide from section actually next to it
- Separator is kept when a preview window can be at 'next' position,
right next to the input section
The native border is the handle for moving and resizing the pane with
the mouse, so use it by default, consistent with tmux. fzf draws its
own border only when a border style is explicitly specified with
--border. Extract the shared native-border decision into a helper.
The pane sets the options itself before running fzf, so that they are
in place no matter how quickly the command exits, targeted at
$TMUX_PANE; the default target would resolve to the active pane of
the session's current window.
- pane-border-format is set to '#{pane_title}' so that the label is
displayed on the border when pane-border-status is enabled;
pane-border-status itself is a window option in released tmux
versions and is left alone
- When a border style is explicitly specified with --border, a popup
is used instead of a floating pane so that the fzf-drawn border is
the only border shown; give 'border-native' to force a floating pane
- 'none' and 'line' are treated as no border; fzf draws no box for
either, so the label is displayed on the native border
- Remove 'border-fzf' which is now redundant; it was never released
- The title is escaped for select-pane -T which expands format
expressions; a lone ';' is escaped as tmux would treat it as a
command separator
- The label is skipped when ANSI stripping leaves an empty string
- --border-label-pos is ignored
- Fix remain-on-exit set on the original pane instead of the floating
pane
Unlike a popup, a floating pane is not modal; you can switch to other
panes and windows while fzf is running, move and resize the pane with
the mouse, zoom it to fullscreen, and use copy-mode in it.
- Floating pane always has a native border, so 'border-native' is
implied; give new 'border-fzf' option to fall back to a popup where
fzf draws its own border
- Popup is also used on tmux versions below 3.7, or when the window is
too small to fit a floating pane
- new-pane does not block until the command finishes and does not
propagate the exit status; block on a wait-for channel signaled by
the pane and pass the exit status through a temporary file
- Watchdog process signals the channel when the pane is closed
abnormally (e.g. kill-pane)
- Kill the pane when the proxy process is interrupted, like a popup
dying with its client
- Unzoom the window before creating the floating pane; doing so over a
zoomed window crashes the tmux server on 3.7b, and newer versions of
tmux unzoom the window anyway
- Floating pane size excludes the border and the position is that of
the content area; treat the requested size as the total footprint
including the border for consistency with popups
- Close the pane on exit even when remain-on-exit is on
- Pre-create the exit status file with O_EXCL to prevent tampering on
a shared TMPDIR
Blocks execution of subsequent actions until the current search
completes, so motion actions operate on the complete result set.
# Move to the best match only after the search is done
fzf --bind 'start:change-query(foo)+wait+best'
- Blocks when a search is in progress, will be triggered by preceding
actions, or the query was edited in the same binding; no-op otherwise
- Initial input load counts as a search in progress, so 'start:wait'
blocks until the input is fully loaded and searched
- User input ignored while blocked, including --expect keys; keys bound
to abort/cancel cancel the wait and discard pending actions instead
- Actions deferred by the wait survive across nested lists (trigger
chords) and cancel/re-arm chains
- Debounced feedback after 200ms: dimmed prompt, hidden cursor, and
(..) on the info line