diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa5790e..beb43563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ CHANGELOG ```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 ------ diff --git a/README-VIM.md b/README-VIM.md index 70c5b37a..ba505e45 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -1,6 +1,26 @@ FZF Vim integration =================== + + +* [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) + + + Installation ------------ @@ -133,19 +153,35 @@ let g:fzf_action = { \ 'ctrl-v': 'vsplit' } " Default fzf layout -" - Popup window (center of the screen) -let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } +if exists('$TMUX') || exists('$ZELLIJ') + " 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 } } +endif -" - Popup window (center of the current window) +" Here are some more layout examples: + +" - 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 } } -" - Popup window (anchored to the bottom of the current window) +" - Vim 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 } } -" - down / up / left / right +" - Vim split window: down / up / left / right let g:fzf_layout = { 'down': '40%' } -" - Window using a Vim command +" - Vim window using a Vim command let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10new' } @@ -408,10 +444,10 @@ Tips ### fzf inside terminal buffer -On the latest versions of Vim and Neovim, fzf will start in a terminal buffer. -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 Neovim. +When fzf is configured to start in a terminal buffer inside Vim or Neovim, you +may find the default ANSI colors to be different. In that case, configure the +colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` in +Neovim. ```vim " Terminal colors for seoul256 color scheme @@ -442,7 +478,10 @@ else endif ``` -### Starting fzf in a popup window +### Starting fzf in a Vim popup window + +You can configure fzf to start in a Vim popup window by setting the `window` key +in `g:fzf_layout`. ```vim " Required: @@ -458,18 +497,32 @@ endif let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } ``` -Alternatively, you can make fzf open in a popup window (requires tmux 3.3 or -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`. +### Starting fzf in a tmux/Zellij 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. ```vim -" See `--popup` option in `man fzf` for available options -" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] if exists('$TMUX') || exists('$ZELLIJ') + " See `--popup` option in `man fzf` for available options + " [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] let g:fzf_layout = { 'popup': '90%,70%' } else - let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + " Configure the Vim popup window in case not on the multiplexer + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.7 } } endif ``` @@ -477,7 +530,8 @@ endif 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 -the window. +the window. This applies to the layouts that open inside Vim, not to the tmux +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': '40%'}`), you might want to temporarily disable the statusline for a cleaner diff --git a/doc/fzf.txt b/doc/fzf.txt index 46206282..5828c1e4 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -14,7 +14,8 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap| Tips |fzf-tips| fzf inside terminal buffer |fzf-inside-terminal-buffer| - Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window| + Starting fzf in a Vim popup window |fzf-starting-fzf-in-a-vim-popup-window| + Starting fzf in a tmux/Zellij popup window |fzf-starting-fzf-in-a-tmuxzellij-popup-window| Hide statusline |fzf-hide-statusline| License |fzf-license| @@ -161,19 +162,35 @@ Examples~ \ 'ctrl-v': 'vsplit' } " Default fzf layout - " - Popup window (center of the screen) - let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + if exists('$TMUX') || exists('$ZELLIJ') + " 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 } } + endif - " - Popup window (center of the current window) + " Here are some more layout examples: + + " - 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 } } - " - Popup window (anchored to the bottom of the current window) + " - Vim 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 } } - " - down / up / left / right + " - Vim split window: down / up / left / right let g:fzf_layout = { 'down': '40%' } - " - Window using a Vim command + " - Vim window using a Vim command let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10new' } @@ -418,8 +435,8 @@ TIPS *fzf-tips* *fzf-inside-terminal-buffer* -On the latest versions of Vim and Neovim, fzf will start in a terminal buffer. -If you find the default ANSI colors to be different, consider configuring the +When fzf is configured to start in a terminal buffer inside Vim or Neovim, you +may find the default ANSI colors to be different. In that case, configure the colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` in Neovim. @@ -452,8 +469,11 @@ in Neovim. endif < -< Starting fzf in a popup window >____________________________________________~ - *fzf-starting-fzf-in-a-popup-window* +< Starting fzf in a Vim popup window >________________________________________~ + *fzf-starting-fzf-in-a-vim-popup-window* + +You can configure fzf to start in a Vim popup window by setting the `window` +key in `g:fzf_layout`. > " Required: " - width [float range [0 ~ 1]] or [integer range [8 ~ ]] @@ -467,16 +487,33 @@ in Neovim. " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } < -Alternatively, you can make fzf open in a popup window (requires tmux 3.3 or -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`. + +< Starting fzf in a tmux/Zellij popup window >________________________________~ + *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. > - " See `--popup` option in `man fzf` for available options - " [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] if exists('$TMUX') || exists('$ZELLIJ') + " See `--popup` option in `man fzf` for available options + " [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]] let g:fzf_layout = { 'popup': '90%,70%' } else - let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + " Configure the Vim popup window in case not on the multiplexer + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.7 } } endif < @@ -485,7 +522,8 @@ above, or Zellij 0.44 or above) by putting `--popup` options in `popup` key. 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 -the window. +the window. This applies to the layouts that open inside Vim, not to the tmux +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': '40%'}`), you might want to temporarily disable the statusline for a cleaner diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 59a540ef..e0dbd64f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -140,6 +140,11 @@ function! s:popup_support() endfunction 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() \ ? { 'window' : { 'width': 0.9, 'height': 0.6 } } \ : { 'down': '~40%' } @@ -165,6 +170,12 @@ function! fzf#install() if v:shell_error throw 'Failed to download fzf: '.script 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 let s:versions = {} @@ -257,7 +268,7 @@ function! fzf#exec(...) 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. +" 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') @@ -273,14 +284,7 @@ function! s:tmux_enabled() 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') + if empty($TMUX) && empty($ZELLIJ) return 0 endif @@ -288,7 +292,21 @@ function! s:tmux_enabled() return s:tmux endif - let s:tmux = 0 + let [s:tmux, s:tmux_floating] = [0, 0] + + " --popup covers Zellij as well, where the fzf-tmux script and the tmux + " version are irrelevant. fzf learned it in 0.71.0, and the floating pane + " options it passes need Zellij 0.44 or above. fzf checks tmux first, so + " this branch is Zellij without tmux. Both non-empty means tmux wins. + " empty(), not exists(), to match how fzf reads the two variables + 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 + let output = system('tmux -V') if v:shell_error return s:tmux @@ -296,9 +314,15 @@ function! s:tmux_enabled() " 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 + " --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 @@ -451,7 +475,10 @@ function! fzf#wrap(...) if !exists('g:fzf_layout') && exists('g:fzf_height') let opts.down = g:fzf_height else - let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout()))) + " Not get(), which would evaluate s:default_layout() and run its version + " 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 @@ -627,6 +654,57 @@ function! s:present(dict, ...) return 0 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) let size = get(a:dict, 'tmux', '') if empty(size) diff --git a/test/vim/fzf.vader b/test/vim/fzf.vader index 28115d0b..402a6d99 100644 --- a/test/vim/fzf.vader +++ b/test/vim/fzf.vader @@ -102,9 +102,36 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir): " Working directory changed due to &acd 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): 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') Log opts AssertEqual 0.9, opts.window.width