- 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.
ESCDELAY=0 makes the retry count zero, so the wait added in d0377ed had
no effect and the reply still leaked. Past the introducer these bytes
cannot be the ESC key, so waiting for them adds no Escape latency.
- Wait at least defaultEscDelay, honour a larger ESCDELAY
- Lone ESC still honours ESCDELAY=0, measured 3ms to exit either way
Fix#4899
Read loop dropped its escDelay retry budget after every successful byte,
so a sequence split across reads reached the parser as a fragment, parsed
as ALT-[ with the remainder left behind as query text.
- fzf queries DECRQM at startup since dab626b, so a terminal answering
late leaked "?2004;2$y" into the query
- Same split leaked modified keys and mouse sequences: CTRL-UP left "5A",
SGR mouse left "0;1;1M"
- Bound unchanged, a stall longer than escDelay still falls back to ALT
Fix#4899
* 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.
escapeArg only doubles a backslash that precedes a quote or ends the
argument, the same rule as syscall.EscapeArg, and @ is not a cmd
metacharacter so it is not caret-escaped. The Windows expectations here
still asked for both. Checked by round-tripping each entry through
cmd.exe into a program's argv: all ten come back byte for byte.
The template writes to os.CreateTemp("", "fzf-temp-*"); the fzf-preview-
prefix went away with the --tmux work. The digit count was also pinned at
nine, but CreateTemp's suffix has no fixed width.
The file is opened under t.TempDir() and never closed, so the cleanup
that TempDir registers cannot remove it on Windows, where an open file
cannot be unlinked. The test itself passes; it is the cleanup that fails
the run. history_test.go and options_test.go already close theirs.
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
ToRunes aliases the rune array, and the editing actions append into
t.input in place when the cursor is not at the end, so the keystrokes
edit the item. Non-ASCII items only, ASCII gets a fresh slice.
printf '한글abcde\n' | fzf --bind 'ctrl-y:replace-query'
ctrl-y, Left, BSpace, ctrl-u -> 한글abcee
Runes and ToRunes are now documented read-only. A stale fold bit was the
other symptom, letting the prefilter reject an item the general path
matches.
The scan only ran for ASCII patterns, so searching CJK text with a CJK
query still built the full score matrix. Scan for one byte of the pattern
rune and verify all four.
Which byte matters. Every ASCII rune contributes three zero bytes, so
U+AE00 scanned by its zero low byte hits on nearly every character of an
ASCII-heavy line. Pick a byte that cannot occur in an ASCII rune, else
any non-zero one.
A non-ASCII pattern rune is safe only when no other rune lowercases onto
it. Uncased is not sufficient: U+00DF has no simple uppercase, yet U+1E9E
lowercases to it, so the foldable set is excluded too.
Measured on 1.4M-line corpora, with a non-ASCII query:
- Every line CJK: 5.1x to 10.2x
- Mostly-ASCII paths behind a Hangul prefix: 5.6x to 6.0x, and 1.2x
where every line matches so nothing can be rejected
- ASCII queries unchanged, kept off the non-inlinable guard
normalizeRune guarded with 0x00C0..0xFF61, which does not exclude Hangul,
CJK or Cyrillic, so every rune of those scripts hashed into the map only
to miss. Every key of the map folds to ASCII, so the bitmap added for the
rune prefilter rejects them without a lookup.
- Non-ASCII queries 1.21x where every line is CJK, 1.05x on mostly-ASCII
paths behind a Hangul prefix
- ASCII queries unchanged, the prefilter already skips Phase 2 for them
- Normalization share of query time for a non-ASCII query: 17.2% -> 0%
asciiFuzzyIndex gave up on non-ASCII lines, so every item ran the full
score matrix. A []rune is a fixed 4-byte stride, so the SIMD byte
scanners can run over it directly: find the low byte, then confirm
4-byte alignment and three zero bytes.
Case folding and normalization can turn a non-ASCII rune into the ASCII
char being searched, which the scan cannot see. ToChars now flags lines
holding such a rune and those keep the old path. Normalization is
Latin-only, so Hangul, CJK, Cyrillic, Greek, Hebrew, Arabic, Thai, kana
and emoji never set the flag.
Chars had no spare padding, so inBytes moves into a flags byte.
Measured on 1.4M-line corpora:
- Mostly-ASCII paths behind a Hangul prefix: 'conf' 1.8x, 'binutils'
2.9x, 'ltversion' 4.0x, no-match 8.4x
- Every line CJK: 17x on both matching and non-matching queries
- ASCII input unchanged, non-ASCII patterns not covered yet
utf8.DecodeRune already fast-paths ASCII, but it is too complex to inline
(cost 201 against a budget of 80), so a mostly-ASCII line pays one call
per byte just to be told the byte is ASCII.
Only the run after the first non-ASCII byte reaches the decode loop, so
the gain depends on where that byte falls.
- 70-rune ASCII line: 145ns -> 42ns in the decode loop
- Ingestion of 1.4M mostly-ASCII paths behind a Hangul prefix, where the
loop covers the whole line: 377ms -> 233ms
- The same paths with the Hangul at the end, where it covers six bytes:
207ms -> 196ms
- Break-even sits at ~100% non-ASCII runes: still 1.01x at 95%. Only a
line holding no ASCII byte at all loses, by ~0.14ns per rune, ~5% of
the loop
- Capacity was byte length, over-allocating by bytes-per-rune (2-4x)
- Count non-continuation bytes with SWAR before allocating
- Invalid bytes undercount, never overcount, so append covers the gap
- Query performance unchanged, this is a memory fix
- The gain tracks bytes-per-rune, the cost tracks how much of the line
follows the first non-ASCII byte, so the two move independently
Measured on 1.4M-line corpora:
- Every line CJK: RSS 362MB -> 255MB, ingestion -5%
- Mostly-ASCII paths behind a Hangul prefix: RSS 556MB -> 533MB,
ingestion +3.5%, the counting pass covering the whole line
- The same paths with the Hangul at the end: RSS 563MB -> 535MB,
ingestion +0.9%, the counting pass covering six bytes
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
--preview-window ~N draws the header in its own pass, resuming the body
in the middle of an image: an image arrives as one line but takes up many
rows. ~N is typically global and meant for text, while the same preview
command also renders images.
tmux ends a command at an argument whose last character is ';', so a
--border-label ending in one was stored truncated. escapeTmuxSeparator
only covered a value that was exactly ';'.
fzf --popup --border-label 'foo;' -> @fzf-border-label was 'foo'
--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
A Kitty graphics command is an APC sequence, which tmux takes as a
request to set the pane title. A bare one therefore never reaches the
terminal and overwrites the title on the way, which is visible on the
border of a floating pane. Programs are expected to wrap it in the tmux
passthrough sequence themselves, but 'kitten icat --clear' does not,
unlike the image data it emits right after.
Sixel and iTerm2 sequences are left alone; tmux renders the former
itself and neither affects the title.
Related: #4870
- Vary normalize in the fuzz targets; document why the exhaustive sweep
keeps it fixed (no-op for ASCII input)
- Seed the fuzz targets with patterns of the right length; single-char
seeds were all two-char and got rejected
- Note that the disable* hook forbids parallel test execution
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.
For single ASCII character, jump between occurrences with vectorized
search and score each directly from bonus matrix instead of running
full pipeline with prefilter and score arrays. 2x faster on 100K-line
benchmark corpus (5.4ms -> 2.5ms). Matters most on first keystroke
where candidate set is largest and result cache cannot help.
Phase 3 writes C rows only from column F[r], but tie-breaking in the
backtrace could read row i+1 at column j+1 left of F[i+1]. With reused
slab, that cell holds data from previously processed item, so highlight
positions in equal-score ties depended on processing order.