Compare commits

..
10 changed files with 77 additions and 573 deletions
-17
View File
@@ -1,23 +1,6 @@
CHANGELOG CHANGELOG
========= =========
0.74.4
------
- Fixed an escape sequence split across reads being parsed as a fragment, which leaked the rest into the query (#4899)
- e.g. A terminal answering the startup `DECRQM` query late left `?2004;2$y`, CTRL-UP left `5A`, and SGR mouse input left `0;1;1M`
- Fixed `--tiebreak=pathname` not detecting the last path separator when the line contains a non-ASCII character before it (#4902)
- 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%' }
```
- fzf now opens in a tmux or Zellij floating pane by default, so the window it was started from stays visible and can be used while fzf is running
- Requires tmux 3.7+ or Zellij 0.44+
- Set `g:fzf_layout` to pick a different layout
0.74.3 0.74.3
------ ------
- Performance optimizations for non-ASCII input - Performance optimizations for non-ASCII input
+20 -76
View File
@@ -1,26 +1,6 @@
FZF Vim integration FZF Vim integration
=================== ===================
<!-- vim-markdown-toc GFM -->
* [Installation](#installation)
* [Summary](#summary)
* [`:FZF[!]`](#fzf)
* [Configuration](#configuration)
* [Examples](#examples)
* [Explanation of `g:fzf_colors`](#explanation-of-gfzf_colors)
* [`fzf#run`](#fzfrun)
* [`fzf#wrap`](#fzfwrap)
* [Global options supported by `fzf#wrap`](#global-options-supported-by-fzfwrap)
* [Tips](#tips)
* [fzf inside terminal buffer](#fzf-inside-terminal-buffer)
* [Starting fzf in a Vim popup window](#starting-fzf-in-a-vim-popup-window)
* [Starting fzf in a tmux/Zellij popup window](#starting-fzf-in-a-tmuxzellij-popup-window)
* [Hide statusline](#hide-statusline)
* [License](#license)
<!-- vim-markdown-toc -->
Installation Installation
------------ ------------
@@ -153,35 +133,19 @@ let g:fzf_action = {
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
if exists('$TMUX') || exists('$ZELLIJ') " - Popup window (center of the screen)
" The Vim plugin will try to open fzf in a tmux or Zellij popup let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" if possible (requires recent fzf and tmux/zellij) using --popup option,
" with the following argument:
let g:fzf_layout = { 'popup': '90%,60%' }
else
" If --popup option is not available, it will open in a popup window inside
" Vim (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif
" Here are some more layout examples: " - Popup window (center of the current window)
" - Tmux or Zellij popup at the bottom 40%
let g:fzf_layout = { 'popup': 'bottom,40%' }
" - Tmux or Zellij popup at the top with a different size
let g:fzf_layout = { 'popup': 'top,90%,40%' }
" - Vim popup window: at the center of the current window (relative)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Vim popup window: anchored to the bottom of the current window " - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - Vim split window: down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
" - Vim window using a Vim command " - Window using a Vim command
let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10new' } let g:fzf_layout = { 'window': '10new' }
@@ -332,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%`) |
| `popup`/`tmux` | string | (Layout) `--popup` options (e.g. `90%,70%`) | | `tmux` | string | (Layout) `--tmux` 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}`) |
@@ -444,10 +408,10 @@ Tips
### fzf inside terminal buffer ### fzf inside terminal buffer
When fzf is configured to start in a terminal buffer inside Vim or Neovim, you On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
may find the default ANSI colors to be different. In that case, configure the If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` in colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
Neovim. in Neovim.
```vim ```vim
" Terminal colors for seoul256 color scheme " Terminal colors for seoul256 color scheme
@@ -478,10 +442,7 @@ else
endif endif
``` ```
### Starting fzf in a Vim popup window ### Starting fzf in a popup window
You can configure fzf to start in a Vim popup window by setting the `window` key
in `g:fzf_layout`.
```vim ```vim
" Required: " Required:
@@ -497,32 +458,16 @@ in `g:fzf_layout`.
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
``` ```
### Starting fzf in a tmux/Zellij popup window Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting `--tmux` option value in `tmux` key.
fzf can also start in a popup of the multiplexer instead of a window inside
Vim, by putting a `--popup` option value in the `popup` key. `tmux` is
accepted as a synonym, just as `--tmux` is an alias of `--popup`.
The layout works on tmux 3.3 or above, or on Zellij 0.44 or above with fzf
0.71.0 or above. It is the default on tmux 3.7 or above with fzf 0.74.0 or
above, and on Zellij, where the pane is not modal: Vim keeps the window fzf
was started from visible, and you can switch to it while fzf is open. Below
those versions tmux gives a popup that cannot be left, so a window inside Vim
is the default there. On tmux, an explicit `--border` style also gives a modal
popup rather than a floating pane, because the native border of a tmux
floating pane cannot be removed. Drop `--border` to keep the floating pane and
its native border. Zellij keeps the floating pane either way, and hides its
native border when fzf draws one. Set `g:fzf_layout` yourself to choose either
one.
```vim ```vim
if exists('$TMUX') || exists('$ZELLIJ') " 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')
let g:fzf_layout = { 'popup': '90%,70%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
" Configure the Vim popup window in case not on the multiplexer let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.7 } }
endif endif
``` ```
@@ -530,8 +475,7 @@ endif
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. This applies to the layouts that open inside Vim, not to the tmux the window.
or Zellij pane the default uses, which is not a buffer.
For example, if you open fzf on the bottom on the screen (e.g. `{'down': For example, if you open fzf on the bottom on the screen (e.g. `{'down':
'40%'}`), you might want to temporarily disable the statusline for a cleaner '40%'}`), you might want to temporarily disable the statusline for a cleaner
+18 -57
View File
@@ -14,8 +14,7 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to
Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap| Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap|
Tips |fzf-tips| Tips |fzf-tips|
fzf inside terminal buffer |fzf-inside-terminal-buffer| fzf inside terminal buffer |fzf-inside-terminal-buffer|
Starting fzf in a Vim popup window |fzf-starting-fzf-in-a-vim-popup-window| Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window|
Starting fzf in a tmux/Zellij popup window |fzf-starting-fzf-in-a-tmuxzellij-popup-window|
Hide statusline |fzf-hide-statusline| Hide statusline |fzf-hide-statusline|
License |fzf-license| License |fzf-license|
@@ -162,35 +161,19 @@ Examples~
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
if exists('$TMUX') || exists('$ZELLIJ') " - Popup window (center of the screen)
" The Vim plugin will try to open fzf in a tmux or Zellij popup
" if possible (requires recent fzf and tmux/zellij) using --popup option,
" with the following argument:
let g:fzf_layout = { 'popup': '90%,60%' }
else
" If --popup option is not available, it will open in a popup window inside
" Vim (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif
" Here are some more layout examples: " - Popup window (center of the current window)
" - Tmux or Zellij popup at the bottom 40%
let g:fzf_layout = { 'popup': 'bottom,40%' }
" - Tmux or Zellij popup at the top with a different size
let g:fzf_layout = { 'popup': 'top,90%,40%' }
" - Vim popup window: at the center of the current window (relative)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Vim popup window: anchored to the bottom of the current window " - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - Vim split window: down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
" - Vim window using a Vim command " - Window using a Vim command
let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10new' } let g:fzf_layout = { 'window': '10new' }
@@ -327,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%` )
`popup` / `tmux` | string | (Layout) `--popup` options (e.g. `90%,70%` ) `tmux` | string | (Layout) `--tmux` 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}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
@@ -435,8 +418,8 @@ TIPS *fzf-tips*
*fzf-inside-terminal-buffer* *fzf-inside-terminal-buffer*
When fzf is configured to start in a terminal buffer inside Vim or Neovim, you On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
may find the default ANSI colors to be different. In that case, configure the If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
in Neovim. in Neovim.
@@ -469,11 +452,8 @@ in Neovim.
endif endif
< <
< Starting fzf in a Vim popup window >________________________________________~ < Starting fzf in a popup window >____________________________________________~
*fzf-starting-fzf-in-a-vim-popup-window* *fzf-starting-fzf-in-a-popup-window*
You can configure fzf to start in a Vim popup window by setting the `window`
key in `g:fzf_layout`.
> >
" Required: " Required:
" - width [float range [0 ~ 1]] or [integer range [8 ~ ]] " - width [float range [0 ~ 1]] or [integer range [8 ~ ]]
@@ -487,33 +467,15 @@ key in `g:fzf_layout`.
" - '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
< Starting fzf in a tmux/Zellij popup window >________________________________~ or above) by putting `--tmux` options in `tmux` key.
*fzf-starting-fzf-in-a-tmuxzellij-popup-window*
fzf can also start in a popup of the multiplexer instead of a window inside
Vim, by putting a `--popup` option value in the `popup` key. `tmux` is
accepted as a synonym, just as `--tmux` is an alias of `--popup`.
The layout works on tmux 3.3 or above, or on Zellij 0.44 or above with fzf
0.71.0 or above. It is the default on tmux 3.7 or above with fzf 0.74.0 or
above, and on Zellij, where the pane is not modal: Vim keeps the window fzf
was started from visible, and you can switch to it while fzf is open. Below
those versions tmux gives a popup that cannot be left, so a window inside Vim
is the default there. On tmux, an explicit `--border` style also gives a modal
popup rather than a floating pane, because the native border of a tmux
floating pane cannot be removed. Drop `--border` to keep the floating pane and
its native border. Zellij keeps the floating pane either way, and hides its
native border when fzf draws one. Set `g:fzf_layout` yourself to choose either
one.
> >
if exists('$TMUX') || exists('$ZELLIJ') " 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[%]]
let g:fzf_layout = { 'popup': '90%,70%' } if exists('$TMUX')
let g:fzf_layout = { 'tmux': '90%,70%' }
else else
" Configure the Vim popup window in case not on the multiplexer let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.7 } }
endif endif
< <
@@ -522,8 +484,7 @@ one.
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. This applies to the layouts that open inside Vim, not to the tmux the window.
or Zellij pane the default uses, which is not a buffer.
For example, if you open fzf on the bottom on the screen (e.g. `{'down': For example, if you open fzf on the bottom on the screen (e.g. `{'down':
'40%'}`), you might want to temporarily disable the statusline for a cleaner '40%'}`), you might want to temporarily disable the statusline for a cleaner
+22 -195
View File
@@ -24,8 +24,7 @@
if exists('g:loaded_fzf') if exists('g:loaded_fzf')
finish finish
endif endif
" Bumped when the plugin gains behavior a caller may need to detect let g:loaded_fzf = 1
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
@@ -128,7 +127,7 @@ function! s:fzf_tempname()
return s:fzf_call('tempname') return s:fzf_call('tempname')
endfunction endfunction
let s:layout_keys = ['window', 'popup', 'tmux', 'up', 'down', 'left', 'right'] let s:layout_keys = ['window', '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'
@@ -140,11 +139,6 @@ function! s:popup_support()
endfunction endfunction
function! s:default_layout() function! s:default_layout()
" A floating pane leaves the window fzf was started from visible and
" reachable while fzf is open. A popup covers it, inside Vim or not
if s:floating_pane()
return { 'tmux': '90%,60%' }
endif
return s:popup_support() return s:popup_support()
\ ? { 'window' : { 'width': 0.9, 'height': 0.6 } } \ ? { 'window' : { 'width': 0.9, 'height': 0.6 } }
\ : { 'down': '~40%' } \ : { 'down': '~40%' }
@@ -170,12 +164,6 @@ function! fzf#install()
if v:shell_error if v:shell_error
throw 'Failed to download fzf: '.script throw 'Failed to download fzf: '.script
endif endif
" A new binary invalidates the chosen executable and everything derived from
" its version, including whether fzf opens a floating pane. fzf#install() is
" also the vim-plug 'do' hook, so this can run long after the first fzf call
let [s:versions, s:checked] = [{}, {}]
unlet! s:exec s:tmux s:tmux_floating
endfunction endfunction
let s:versions = {} let s:versions = {}
@@ -267,24 +255,8 @@ 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. --popup 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') if has('gui_running') || !exists('$TMUX')
return 0
endif
if empty($TMUX) && empty($ZELLIJ)
return 0 return 0
endif endif
@@ -292,42 +264,17 @@ function! s:tmux_enabled()
return s:tmux return s:tmux
endif endif
let [s:tmux, s:tmux_floating] = [0, 0] let s:tmux = 0
if !executable(s:fzf_tmux)
" --popup covers Zellij as well, where the fzf-tmux script and the tmux if executable('fzf-tmux')
" version are irrelevant. fzf learned it in 0.71.0, and the floating pane let s:fzf_tmux = 'fzf-tmux'
" options it passes need Zellij 0.44 or above. fzf checks tmux first, so else
" this branch is Zellij without tmux. Both non-empty means tmux wins. return 0
" empty(), not exists(), to match how fzf reads the two variables endif
if empty($TMUX)
let s:tmux =
\ s:compare_versions(s:get_version(s:fzf_binary()), '0.71.0') >= 0
\ && s:compare_versions(s:zellij_version(), '0.44') >= 0
let s:tmux_floating = s:tmux
return s:tmux
endif endif
let output = system('tmux -V') let output = system('tmux -V')
if v:shell_error let s:tmux = !v:shell_error && output >= 'tmux 1.7'
return s:tmux
endif
" e.g. 'tmux 3.7b', 'tmux next-3.8'
let ver = matchstr(output, '\d\+\.\d\+')
" --popup requires tmux 3.3 or above, and needs no fzf-tmux script. The
" default layout wants a floating pane, which also needs fzf 0.74.0 or
" above. fzf opens a modal popup otherwise. The version here only skips the
" probe for servers too old to answer it
if s:compare_versions(ver, '3.3') >= 0
let s:tmux = 1
let s:tmux_floating = s:compare_versions(ver, '3.7') >= 0
\ && s:tmux_floating_pane_info()
\ && s:compare_versions(s:get_version(s:fzf_binary()), '0.74.0') >= 0
return s:tmux
endif
" Older versions still go through the script
let s:tmux = !empty(s:fzf_tmux_script()) && s:compare_versions(ver, '1.7') >= 0
return s:tmux return s:tmux
endfunction endfunction
@@ -475,10 +422,7 @@ function! fzf#wrap(...)
if !exists('g:fzf_layout') && exists('g:fzf_height') if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height let opts.down = g:fzf_height
else else
" Not get(), which would evaluate s:default_layout() and run its version let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout())))
" checks even when g:fzf_layout makes the answer irrelevant
let opts = extend(opts, s:validate_layout(
\ exists('g:fzf_layout') ? g:fzf_layout : s:default_layout()))
endif endif
endif endif
@@ -558,11 +502,6 @@ 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
@@ -600,19 +539,16 @@ 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) && !s:need_cmd_window let use_term = has_nvim_term || has_vim8_term
\ && !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' let optstr .= ' --no-height --no-tmux'
" 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
@@ -632,12 +568,8 @@ try
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
" s:execute_tmux may run fzf asynchronously, so it calls s:callback itself let lines = use_tmux ? s:execute_tmux(dict, command, temps)
if use_tmux \ : s:execute(dict, command, use_height, temps)
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
@@ -654,57 +586,6 @@ function! s:present(dict, ...)
return 0 return 0
endfunction endfunction
" The binary fzf#exec() would choose, without its prompting or installing.
" Layout selection runs before fzf#exec() has resolved one
function! s:fzf_binary()
if exists('s:exec')
return s:exec
endif
let bins = filter(['fzf', s:fzf_go], 'executable(v:val)')
if empty(bins)
return ''
endif
return len(bins) > 1 ? sort(bins, 's:compare_binary_versions')[-1] : bins[0]
endfunction
function! s:zellij_version()
if !exists('s:zellij_ver')
let output = systemlist('zellij --version')
let s:zellij_ver = v:shell_error || empty(output)
\ ? '' : matchstr(output[0], '[0-9.]\+')
endif
return s:zellij_ver
endfunction
" fzf decides on the running server, not on the version the tmux client
" reports, so ask it the same question rather than predicting the answer.
" See tmuxFloatingPaneInfo in src/tmux.go
function! s:tmux_floating_pane_info()
" fzf does not use a floating pane when it was not started from a pane
if empty($TMUX_PANE)
return 0
endif
let out = system('tmux display-message -p -t '.fzf#shellescape($TMUX_PANE)
\ .' ''#{window_width} #{window_height}'' '';'' list-commands new-pane')
" A server that does not know the command exits normally with no output
if v:shell_error || out !~# 'new-pane'
return 0
endif
" Too small for a floating pane of the minimum size
return str2nr(matchstr(out, '^\s*\zs\d\+')) >= 3
\ && str2nr(matchstr(out, '^\s*\d\+\s\+\zs\d\+')) >= 3
endfunction
" Whether fzf will open a floating pane that can be left while fzf runs. A
" tmux popup below 3.7 is modal, and the fzf-tmux script that older tmux
" versions need does not accept a --popup value at all. Without a job to wait
" on fzf, s:execute_tmux() blocks on system() and the window stays visible but
" frozen, so the default keeps fzf inside Vim there
function! s:floating_pane()
return (has('nvim') || has('job'))
\ && s:tmux_enabled() && get(s:, 'tmux_floating', 0)
endfunction
function! s:fzf_tmux(dict) function! s:fzf_tmux(dict)
let size = get(a:dict, 'tmux', '') let size = get(a:dict, 'tmux', '')
if empty(size) if empty(size)
@@ -716,15 +597,10 @@ function! s:fzf_tmux(dict)
endfor endfor
endif endif
" Legacy fzf-tmux options are flags. A --tmux value never starts with a dash, " Legacy fzf-tmux options
" but may contain one, as in '90%,60%,border-native' if size =~ '-'
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(script), size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
end end
" Using native --tmux option " Using native --tmux option
@@ -866,42 +742,6 @@ 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)
@@ -910,24 +750,11 @@ 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)
let lines = s:exit_handler(a:dict, exit_status, command) < 2 ? lines : [] return 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)
+3 -3
View File
@@ -82,10 +82,10 @@ func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int,
val = item.TrimLength() val = item.TrimLength()
case byPathname: case byPathname:
if validOffsetFound { if validOffsetFound {
// Rune index, to be comparable with minBegin
lastDelim := -1 lastDelim := -1
for i := numChars - 1; i >= 0; i-- { s := item.text.ToString()
if r := item.text.Get(i); r == '/' || r == '\\' { for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' || s[i] == '\\' {
lastDelim = i lastDelim = i
break break
} }
-25
View File
@@ -272,28 +272,3 @@ func TestRadixSortResults(t *testing.T) {
} }
} }
} }
func TestPathnameTiebreak(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byPathname}
score := 100
test := func(input string, offset Offset, expected uint16) {
for _, chars := range []util.Chars{util.ToChars([]byte(input)), util.RunesToChars([]rune(input))} {
item := buildResult(withIndex(&Item{text: chars}, 1), []Offset{offset}, score)
if item.points[3] != math.MaxUint16-uint16(score) || item.points[2] != expected {
t.Error(input, item.points, expected)
}
}
}
// Match in the file name
test("x/foo/foo.txt", Offset{6, 9}, 1)
// Match in the directory path
test("x/foo/aa.txt", Offset{2, 5}, math.MaxUint16)
// Offsets are rune indexes, so a multi-byte character before the last
// delimiter must not shift the delimiter position
test("一x/foo/foo.txt", Offset{7, 10}, 1)
test("一x/foo/aa.txt", Offset{3, 6}, math.MaxUint16)
}
-6
View File
@@ -29,9 +29,6 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
t.Setenv("SHELL", "cmd")
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := [3][]*Item{{item1}, {item1}, nil} items1 := [3][]*Item{{item1}, {item1}, nil}
items2 := [3][]*Item{ items2 := [3][]*Item{
@@ -258,9 +255,6 @@ func TestQuoteEntry(t *testing.T) {
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`} unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`} windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`}
var effectiveStyle quotes var effectiveStyle quotes
// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
t.Setenv("SHELL", "cmd")
exec := util.NewExecutor("") exec := util.NewExecutor("")
if util.IsWindows() { if util.IsWindows() {
-47
View File
@@ -26,7 +26,6 @@ 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
) )
@@ -339,45 +338,6 @@ 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)
} }
@@ -418,13 +378,6 @@ 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
@@ -1,53 +0,0 @@
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)
}
}
}
+12 -92
View File
@@ -4,44 +4,15 @@ 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 = g:FzfRun({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) let result = fzf#run({ '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(g:FzfRun({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ '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
@@ -51,32 +22,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(g:FzfRun({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ '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(g:FzfRun({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(fzf#run({ '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(g:FzfRun({ 'source': ['hello', 'world'], 'options': '-f e' })) let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' }))
AssertEqual ['hello'], result AssertEqual ['hello'], result
let result = sort(g:FzfRun({ 'source': ['hello', 'world'], 'options': '-f o' })) let result = sort(fzf#run({ '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(g:FzfRun({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(fzf#run({ '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 g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) call fzf#run({'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 g:FzfRun({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'}) call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
tabclose tabclose
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
@@ -84,13 +55,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 g:FzfRun({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'}) call fzf#run({'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 g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) call fzf#run({'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()
@@ -98,40 +69,13 @@ 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 g:FzfRun({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'}) call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'})
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
Execute (Default layout):
unlet! g:fzf_layout g:fzf_height
let layout_keys = ['window', 'popup', 'tmux', 'up', 'down', 'left', 'right']
let opts = fzf#wrap('foobar')
Log opts
let found = filter(copy(layout_keys), 'has_key(opts, v:val)')
AssertEqual 1, len(found)
if found[0] ==# 'tmux'
" Only where fzf opens a floating pane, which can be left while fzf runs
Assert !empty($TMUX) || !empty($ZELLIJ)
AssertEqual '90%,60%', opts.tmux
elseif found[0] ==# 'window'
AssertEqual 0.9, opts.window.width
else
" No popup support in this build
AssertEqual '~40%', opts.down
endif
" Fullscreen strips it, whichever it was
let opts = fzf#wrap('foobar', {}, 1)
Log opts
AssertEqual [], filter(copy(layout_keys), 'has_key(opts, v:val)')
Execute (fzf#wrap): Execute (fzf#wrap):
AssertThrows fzf#wrap({'foo': 'bar'}) AssertThrows fzf#wrap({'foo': 'bar'})
" Pin the layout so the assertions do not depend on the environment
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
let opts = fzf#wrap('foobar') let opts = fzf#wrap('foobar')
Log opts Log opts
AssertEqual 0.9, opts.window.width AssertEqual 0.9, opts.window.width
@@ -206,30 +150,6 @@ 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')