Compare commits

...
3 Commits
Author SHA1 Message Date
Junegunn Choi d46bfd005a Keep a minimum wait for a sequence in flight
CodeQL / Analyze (go) (push) Canceled after 0s
build / build (push) Canceled after 0s
Test fzf on macOS / build (push) Canceled after 0s
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
2026-08-24 09:54:09 +09:00
Junegunn Choi d0377ed4dc Wait for rest of escape sequence before parsing (#4901)
CodeQL / Analyze (go) (push) Canceled after 0s
build / build (push) Canceled after 0s
Test fzf on macOS / build (push) Canceled after 0s
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
2026-08-24 00:40:06 +09:00
Junegunn Choi 5cb7bab702 Run fzf asynchronously in the Vim plugin (#4897)
* 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.
2026-08-23 23:23:51 +09:00
7 changed files with 307 additions and 45 deletions
+11
View File
@@ -1,6 +1,17 @@
CHANGELOG CHANGELOG
========= =========
0.74.4
------
- Vim plugin
- fzf no longer blocks the editor, so live previews keep working while fzf is open
- `fzf#run` returns an empty list when it runs fzf asynchronously. Use `sink`, `sinklist`, or `exit` to get the result
- The popup layout now works under Zellij
- Added `popup` as a synonym of the `tmux` layout key
```vim
let g:fzf_layout = { 'popup': '90%,70%' }
```
0.74.3 0.74.3
------ ------
- Performance optimizations for non-ASCII input - Performance optimizations for non-ASCII input
+8 -6
View File
@@ -296,7 +296,7 @@ The following table summarizes the available options.
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) | | `popup`/`tmux` | string | (Layout) `--popup` options (e.g. `90%,70%`) |
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) | | `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
@@ -458,14 +458,16 @@ endif
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
``` ```
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a popup window (requires tmux 3.3 or
or above) by putting `--tmux` option value in `tmux` key. above, or Zellij 0.44 or above) by putting `--popup` option value in `popup`
key. `tmux` is accepted as a synonym, just as `--tmux` is an alias of
`--popup`.
```vim ```vim
" See `--tmux` option in `man fzf` for available options " See `--popup` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] " [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX') || exists('$ZELLIJ')
let g:fzf_layout = { 'tmux': '90%,70%' } let g:fzf_layout = { 'popup': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif
+7 -6
View File
@@ -310,7 +310,7 @@ The following table summarizes the available options.
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` ) `popup` / `tmux` | string | (Layout) `--popup` options (e.g. `90%,70%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
@@ -467,13 +467,14 @@ in Neovim.
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
< <
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a popup window (requires tmux 3.3 or
or above) by putting `--tmux` options in `tmux` key. above, or Zellij 0.44 or above) by putting `--popup` options in `popup` key.
`tmux` is accepted as a synonym, just as `--tmux` is an alias of `--popup`.
> >
" See `--tmux` option in `man fzf` for available options " See `--popup` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] " [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX') || exists('$ZELLIJ')
let g:fzf_layout = { 'tmux': '90%,70%' } let g:fzf_layout = { 'popup': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif
+115 -20
View File
@@ -24,7 +24,8 @@
if exists('g:loaded_fzf') if exists('g:loaded_fzf')
finish finish
endif endif
let g:loaded_fzf = 1 " Bumped when the plugin gains behavior a caller may need to detect
let g:loaded_fzf = 20260821
let s:is_win = has('win32') || has('win64') let s:is_win = has('win32') || has('win64')
if s:is_win && &shellslash if s:is_win && &shellslash
@@ -127,7 +128,7 @@ function! s:fzf_tempname()
return s:fzf_call('tempname') return s:fzf_call('tempname')
endfunction endfunction
let s:layout_keys = ['window', 'tmux', 'up', 'down', 'left', 'right'] let s:layout_keys = ['window', 'popup', 'tmux', 'up', 'down', 'left', 'right']
let s:fzf_go = s:base_dir.'/bin/fzf' let s:fzf_go = s:base_dir.'/bin/fzf'
let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux' let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
@@ -255,8 +256,31 @@ function! fzf#exec(...)
return s:exec return s:exec
endfunction endfunction
" Path to the fzf-tmux script, or an empty string if it is not available. Only
" the legacy options still need it. --tmux is handled by fzf itself.
function! s:fzf_tmux_script()
if !executable(s:fzf_tmux)
if !executable('fzf-tmux')
return ''
endif
let s:fzf_tmux = 'fzf-tmux'
endif
return s:fzf_tmux
endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
if has('gui_running') || !exists('$TMUX') if has('gui_running')
return 0
endif
" --tmux covers Zellij as well, where the fzf-tmux script and the tmux
" version are irrelevant, but the binary only learned it in 0.71.0
if exists('$ZELLIJ')
return exists('s:exec')
\ && s:compare_versions(s:get_version(s:exec), '0.71.0') >= 0
endif
if !exists('$TMUX')
return 0 return 0
endif endif
@@ -265,16 +289,21 @@ function! s:tmux_enabled()
endif endif
let s:tmux = 0 let s:tmux = 0
if !executable(s:fzf_tmux) let output = system('tmux -V')
if executable('fzf-tmux') if v:shell_error
let s:fzf_tmux = 'fzf-tmux' return s:tmux
else
return 0
endif endif
" e.g. 'tmux 3.7b', 'tmux next-3.8'
let ver = matchstr(output, '\d\+\.\d\+')
" --tmux requires tmux 3.3 or above, and needs no fzf-tmux script
if s:compare_versions(ver, '3.3') >= 0
let s:tmux = 1
return s:tmux
endif endif
let output = system('tmux -V') " Older versions still go through the script
let s:tmux = !v:shell_error && output >= 'tmux 1.7' let s:tmux = !empty(s:fzf_tmux_script()) && s:compare_versions(ver, '1.7') >= 0
return s:tmux return s:tmux
endfunction endfunction
@@ -502,6 +531,11 @@ try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
let dict = exists('a:1') ? copy(a:1) : {} let dict = exists('a:1') ? copy(a:1) : {}
" 'popup' and 'tmux' are synonyms, as --popup and --tmux are. Normalize here
" so that the rest of the function only has to know about one of them.
if has_key(dict, 'popup')
let dict.tmux = remove(dict, 'popup')
endif
let temps = { 'result': s:fzf_tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = s:evaluate_opts(get(dict, 'options', '')) let optstr = s:evaluate_opts(get(dict, 'options', ''))
try try
@@ -539,16 +573,19 @@ try
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || has_vim8_term let use_term = (has_nvim_term || has_vim8_term) && !s:need_cmd_window
\ && !s:need_cmd_window
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_term if use_term
let optstr .= ' --no-height --no-tmux' let optstr .= ' --no-height'
" Cancel a --popup from $FZF_DEFAULT_OPTS only when the spec asks for a Vim
" window. Without a layout option, respect the user's preference.
if s:present(dict, 'window', 'up', 'down', 'left', 'right')
let optstr .= ' --no-tmux'
endif
elseif use_height elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --no-tmux --height='.height let optstr .= ' --no-tmux --height='.height
@@ -568,8 +605,12 @@ try
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let lines = use_tmux ? s:execute_tmux(dict, command, temps) " s:execute_tmux may run fzf asynchronously, so it calls s:callback itself
\ : s:execute(dict, command, use_height, temps) if use_tmux
return s:execute_tmux(dict, command, temps)
endif
let lines = s:execute(dict, command, use_height, temps)
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
@@ -597,10 +638,15 @@ function! s:fzf_tmux(dict)
endfor endfor
endif endif
" Legacy fzf-tmux options " Legacy fzf-tmux options are flags. A --tmux value never starts with a dash,
if size =~ '-' " but may contain one, as in '90%,60%,border-native'
if size =~ '^-'
let script = s:fzf_tmux_script()
if empty(script)
throw 'fzf-tmux not found, required for the legacy option: ' . size
endif
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, fzf#shellescape(script), size, (has_key(a:dict, 'source') ? '' : '-'))
end end
" Using native --tmux option " Using native --tmux option
@@ -742,6 +788,42 @@ function! s:execute(dict, command, use_height, temps) abort
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : [] return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
endfunction endfunction
" Returns 0 if the job could not be started
function! s:start_popup_job(dict, command, temps) abort
let fzf = { 'dict': a:dict, 'temps': a:temps, 'command': a:command }
" Vim passes (job, status) and Nvim (id, status, event)
function! fzf.on_exit(id, code, ...) abort
redraw!
let lines = s:collect(self.temps)
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
return
endif
call s:pushd(self.dict)
call s:callback(self.dict, lines)
endfunction
if has('nvim')
return jobstart([&shell, &shellcmdflag, a:command], fzf) > 0
endif
" The command redirects to the result file, so no stream is of interest
let opts = { 'exit_cb': function(fzf.on_exit),
\ 'in_io': 'null', 'out_io': 'null', 'err_io': 'null' }
" job_start() gives the child $TERM=dumb, and the popup inherits the
" environment from here, so fzf would lose its colors. 'env' was added in
" 8.0.902, so set it in the command itself on older versions.
let command = a:command
if has('patch-8.0.902')
let opts.env = { 'TERM': $TERM }
elseif !s:is_win
let command = join(['export TERM=' . fzf#shellescape($TERM) . ';', command])
endif
let job = job_start([&shell, &shellcmdflag, command], opts)
return job_status(job) !=# 'fail'
endfunction
function! s:execute_tmux(dict, command, temps) abort function! s:execute_tmux(dict, command, temps) abort
let command = a:command let command = a:command
let cwd = s:pushd(a:dict) let cwd = s:pushd(a:dict)
@@ -750,11 +832,24 @@ function! s:execute_tmux(dict, command, temps) abort
let command = join(['cd', fzf#shellescape(cwd), '&&', command]) let command = join(['cd', fzf#shellescape(cwd), '&&', command])
endif endif
" fzf draws in a pane of its own, so the process we start here only waits for
" it. Hold it with a job instead of system() so that Vim keeps processing its
" event loop, and nothing has to be displayed for it. Fall back to the
" blocking path when the job cannot be started, or the sink would never run
" and the temporary files would be left behind.
if (has('nvim') || has('job')) && s:start_popup_job(a:dict, command, a:temps)
" Restore the working directory while fzf runs. on_exit pushes it again
call s:dopopd()
return []
endif
call system(command) call system(command)
let exit_status = v:shell_error let exit_status = v:shell_error
redraw! redraw!
let lines = s:collect(a:temps) let lines = s:collect(a:temps)
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : [] let lines = s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
call s:callback(a:dict, lines)
return lines
endfunction endfunction
function! s:calc_size(max, val, dict) function! s:calc_size(max, val, dict)
+47
View File
@@ -26,6 +26,7 @@ const (
offsetPollTries = 10 offsetPollTries = 10
queryTimeout = 500 * time.Millisecond queryTimeout = 500 * time.Millisecond
maxInputBuffer = 1024 * 1024 maxInputBuffer = 1024 * 1024
escapeLookback = 256
maxSelectTries = 100 maxSelectTries = 100
) )
@@ -338,6 +339,45 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
// Bytes of a CSI sequence: parameter and intermediate bytes continue it, a
// final byte ends it. Order is not enforced. Strictness would only make fzf
// give up on a sequence it could have framed.
//
// https://vt100.net/emu/dec_ansi_parser
func csiContinues(b byte) bool { return b >= 0x20 && b <= 0x3f }
func csiFinal(b byte) bool { return b >= 0x40 && b <= 0x7e }
// incompleteEscape reports whether the buffer ends in an escape sequence that
// has not been terminated yet. The read loop keeps waiting in that case, so the
// parser is never handed a fragment to guess at.
func incompleteEscape(buffer []byte) bool {
// Only the tail can hold a sequence still arriving. This runs once per byte
// read, so scanning all of a large paste would make the read quadratic.
tail := buffer
if len(tail) > escapeLookback {
tail = tail[len(tail)-escapeLookback:]
}
start := bytes.LastIndexByte(tail, Esc.Byte())
if start < 0 || len(tail)-start < 2 {
return false
}
switch tail[start+1] {
case '[':
for _, b := range tail[start+2:] {
if csiFinal(b) {
return false
}
if !csiContinues(b) {
return false // malformed, do not wait for a terminator
}
}
return true
case 'O':
return len(tail)-start < 3
}
return false
}
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) { func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
return r.getBytesInternal(cancellable, r.buffer, false) return r.getBytesInternal(cancellable, r.buffer, false)
} }
@@ -378,6 +418,13 @@ func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblo
retries = 0 retries = 0
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
// Keep waiting while a sequence is still arriving. Dropping the budget
// after every byte left fzf parsing whatever the read happened to end on.
// Past the introducer this is not the ESC key, so the wait costs no
// Escape latency and ESCDELAY=0 must not reduce it to nothing.
if retries == 0 && incompleteEscape(buffer) {
retries = max(r.escDelay, defaultEscDelay) / escPollInterval
}
pc = c pc = c
// This should never happen under normal conditions, // This should never happen under normal conditions,
+53
View File
@@ -0,0 +1,53 @@
package tui
import (
"strings"
"testing"
)
func TestIncompleteEscape(t *testing.T) {
for _, c := range []struct {
buffer string
want bool
}{
// Complete sequences: nothing to wait for
{"\x1b[A", false},
{"\x1bOA", false},
{"\x1b[1;5A", false},
{"\x1b[200~", false},
{"\x1b[<0;1;1M", false},
{"\x1b[12;34R", false},
{"\x1b[?2004;2$y", false},
{"\x1b[?1;2c", false},
// Fragments: keep waiting
{"\x1b[", true},
{"\x1b[?", true},
{"\x1b[1;", true},
{"\x1b[?2004;2$", true},
{"\x1bO", true},
{"\x1b[<0;1;", true},
// Only the trailing sequence matters
{"ab\x1b[?2004;2$", true},
{"\x1b[A\x1b[", true},
{"\x1b[A\x1b[B", false},
// Long buffers: only the tail is scanned, so an introducer further
// back than escapeLookback is not waited for
{strings.Repeat("a", 100000), false},
{"\x1b[" + strings.Repeat("a", 100000), false},
{strings.Repeat("a", 100000) + "\x1b[1;", true},
// Not a sequence fzf waits on
{"", false},
{"abc", false},
{"\x1b", false}, // lone ESC, handled by the existing escDelay branch
{"\x1ba", false}, // ALT-a
{"\x1b[\x01", false}, // malformed, do not stall on it
} {
if got := incompleteEscape([]byte(c.buffer)); got != c.want {
t.Errorf("incompleteEscape(%q) = %v, want %v", c.buffer, got, c.want)
}
}
}
+65 -12
View File
@@ -4,15 +4,44 @@ Execute (Setup):
Log 'Test directory: ' . g:dir Log 'Test directory: ' . g:dir
Save &acd Save &acd
" fzf#run runs fzf in a terminal buffer and returns immediately, so the
" assertions have to wait for the run to finish. 'exit' is called from the
" same handler that runs the sink, just before it, so once it has fired the
" sink has run too. It also fires when nothing is selected, unlike the sink.
function! g:FzfExit(code)
let g:fzf_done = 1
endfunction
" Collects the output without displacing a 'sink' the spec already has.
" s:callback runs both
function! g:FzfCollect(lines)
call extend(g:fzf_lines, a:lines)
endfunction
function! g:FzfRun(spec) abort
let g:fzf_done = 0
let g:fzf_lines = []
call fzf#run(extend(copy(a:spec),
\ { 'exit': function('g:FzfExit'), 'sinklist': function('g:FzfCollect') }))
let started = reltime()
while !g:fzf_done && reltimefloat(reltime(started)) < 10
sleep 10m
endwhile
if !g:fzf_done
throw 'Timed out waiting for fzf#run'
endif
return g:fzf_lines
endfunction
Execute (fzf#run with dir option): Execute (fzf#run with dir option):
let cwd = getcwd() let cwd = getcwd()
let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) let result = g:FzfRun({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
AssertEqual 0, haslocaldir() AssertEqual 0, haslocaldir()
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
execute 'lcd' fnameescape(cwd) execute 'lcd' fnameescape(cwd)
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) let result = sort(g:FzfRun({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
AssertEqual 1, haslocaldir() AssertEqual 1, haslocaldir()
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
@@ -22,32 +51,32 @@ Execute (fzf#run with Funcref command):
function! g:FzfTest(e) function! g:FzfTest(e)
call add(g:ret, a:e) call add(g:ret, a:e)
endfunction endfunction
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) let result = sort(g:FzfRun({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
AssertEqual ['fzf.vader'], sort(g:ret) AssertEqual ['fzf.vader'], sort(g:ret)
Execute (fzf#run with string source): Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(g:FzfRun({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result AssertEqual ['hi'], result
Execute (fzf#run with list source): Execute (fzf#run with list source):
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' })) let result = sort(g:FzfRun({ 'source': ['hello', 'world'], 'options': '-f e' }))
AssertEqual ['hello'], result AssertEqual ['hello'], result
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' })) let result = sort(g:FzfRun({ 'source': ['hello', 'world'], 'options': '-f o' }))
AssertEqual ['hello', 'world'], result AssertEqual ['hello', 'world'], result
Execute (fzf#run with string source): Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(g:FzfRun({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result AssertEqual ['hi'], result
Execute (fzf#run with dir option and noautochdir): Execute (fzf#run with dir option and noautochdir):
set noacd set noacd
let cwd = getcwd() let cwd = getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) call g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
" No change in working directory " No change in working directory
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'}) call g:FzfRun({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
tabclose tabclose
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
@@ -55,13 +84,13 @@ Execute (fzf#run with dir option and noautochdir):
Execute (Incomplete fzf#run with dir option and autochdir): Execute (Incomplete fzf#run with dir option and autochdir):
set acd set acd
let cwd = getcwd() let cwd = getcwd()
call fzf#run({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'}) call g:FzfRun({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'})
" No change in working directory even if &acd is set " No change in working directory even if &acd is set
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
Execute (FIXME: fzf#run with dir option and autochdir): Execute (FIXME: fzf#run with dir option and autochdir):
set acd set acd
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) call g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/foobar', expand('%') AssertEqual '/foobar', expand('%')
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
@@ -69,7 +98,7 @@ Execute (FIXME: fzf#run with dir option and autochdir):
Execute (fzf#run with dir option and autochdir when final cwd is same as dir): Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
set acd set acd
cd /tmp cd /tmp
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'}) call g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'})
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
@@ -150,6 +179,30 @@ Execute (fzf#wrap):
let opts = fzf#wrap({}) let opts = fzf#wrap({})
Assert opts.options =~ '--color=fg:' Assert opts.options =~ '--color=fg:'
Execute (popup is a synonym of tmux):
unlet! g:fzf_layout
" Treated as a layout option, so g:fzf_layout is not applied on top
let opts = fzf#wrap('foobar', {'popup': '90%,70%'})
AssertEqual '90%,70%', opts.popup
Assert !has_key(opts, 'window')
" And stripped in fullscreen, like the others
let opts = fzf#wrap('foobar', {'popup': '90%,70%'}, 1)
Assert !has_key(opts, 'popup')
" Accepted in g:fzf_layout
let g:fzf_layout = {'popup': '90%,70%'}
let opts = fzf#wrap('foobar')
AssertEqual '90%,70%', opts.popup
unlet g:fzf_layout
" Listed as a valid key
let g:fzf_layout = {'bogus': 1}
AssertThrows call fzf#wrap('foobar')
Assert g:vader_exception =~ 'popup'
unlet g:fzf_layout
Execute (fzf#shellescape with sh): Execute (fzf#shellescape with sh):
AssertEqual '''''', fzf#shellescape('', 'sh') AssertEqual '''''', fzf#shellescape('', 'sh')
AssertEqual '''\''', fzf#shellescape('\', 'sh') AssertEqual '''\''', fzf#shellescape('\', 'sh')