mirror of
https://github.com/junegunn/fzf.git
synced 2026-09-13 22:52:01 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3900dd17e4 | ||
|
|
5e73c2ddd3 | ||
|
|
18e5009e0f |
@@ -3,15 +3,6 @@ CHANGELOG
|
|||||||
|
|
||||||
0.74.4
|
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)
|
|
||||||
- Fixed `progress` in the `--listen` status payload staying at 100 while a new search was running, which made a snapshot with a new query and the previous result set look complete (#4903)
|
|
||||||
- It is now reset when a search starts and reaches 100 on the final result, so `progress` of 100 means the matches belong to the query reported next to them
|
|
||||||
- Fixed adaptive height not reserving a line for the divider of an inline header or footer border, so the list came up one line short for each of them (#4904)
|
|
||||||
- e.g. `seq 10 | fzf --height=~100% --list-border --header-lines=1 --header-lines-border=inline`
|
|
||||||
- Fixed fzf erasing the line the prompt was on when it exits, which made the last line of the prompt flicker in fish, bash, and nushell (#4913)
|
|
||||||
- Fixed fzf exiting with status 2 while waiting for a key, when `--listen` is used and 100+ signals interrupt the wait (#4917)
|
|
||||||
- Vim plugin
|
- Vim plugin
|
||||||
- fzf no longer blocks the editor, so live previews keep working while fzf is open
|
- 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
|
- `fzf#run` returns an empty list when it runs fzf asynchronously. Use `sink`, `sinklist`, or `exit` to get the result
|
||||||
@@ -20,16 +11,6 @@ CHANGELOG
|
|||||||
```vim
|
```vim
|
||||||
let g:fzf_layout = { 'popup': '90%,70%' }
|
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
|
|
||||||
- fish
|
|
||||||
- Fixed custom CTRL-T command not using the prefixed target directory in some cases (#4498) (@bitraid)
|
|
||||||
- Optimized description alignment of completion items (#4910) (@bitraid)
|
|
||||||
- nushell
|
|
||||||
- Added key bindings for Helix editing modes, on nushell 0.115.0 or above (#4914) (@sim590)
|
|
||||||
- Fixed CTRL-T inserting the selected paths unquoted
|
|
||||||
- p4p3r (@P4P3R-HAK) reported the security vulnerability and suggested the fix
|
|
||||||
|
|
||||||
0.74.3
|
0.74.3
|
||||||
------
|
------
|
||||||
|
|||||||
+19
-73
@@ -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' }
|
||||||
@@ -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,18 @@ 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 popup window (requires tmux 3.3 or
|
||||||
|
above, or Zellij 0.44 or above) by putting `--popup` option value in `popup`
|
||||||
fzf can also start in a popup of the multiplexer instead of a window inside
|
key. `tmux` is accepted as a synonym, just as `--tmux` is an alias of
|
||||||
Vim, by putting a `--popup` option value in the `popup` key. `tmux` is
|
`--popup`.
|
||||||
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
|
||||||
|
" See `--popup` option in `man fzf` for available options
|
||||||
|
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
if exists('$TMUX') || exists('$ZELLIJ')
|
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%' }
|
let g:fzf_layout = { 'popup': '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 +477,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
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ triggered by a tag push.
|
|||||||
2. Verify file consistency, sign the tag, and push the tag.
|
2. Verify file consistency, sign the tag, and push the tag.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make tag VERSION=0.74.4
|
make tag VERSION=0.74.3
|
||||||
```
|
```
|
||||||
|
|
||||||
`make tag` runs `prerelease` first (checks that the version
|
`make tag` runs `prerelease` first (checks that the version
|
||||||
|
|||||||
+18
-56
@@ -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
|
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' }
|
||||||
@@ -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,16 @@ 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 popup window (requires tmux 3.3 or
|
||||||
< Starting fzf in a tmux/Zellij popup window >________________________________~
|
above, or Zellij 0.44 or above) by putting `--popup` options in `popup` key.
|
||||||
*fzf-starting-fzf-in-a-tmuxzellij-popup-window*
|
`tmux` is accepted as a synonym, just as `--tmux` is an alias of `--popup`.
|
||||||
|
|
||||||
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')
|
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%' }
|
let g:fzf_layout = { 'popup': '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 +485,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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.74.4
|
version=0.74.3
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
$version="0.74.4"
|
$version="0.74.3"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf\-tmux 1 "Sep 2026" "fzf 0.74.4" "fzf\-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "Aug 2026" "fzf 0.74.3" "fzf\-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf\-tmux - open fzf in tmux split pane
|
fzf\-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
+1
-4
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Sep 2026" "fzf 0.74.4" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Aug 2026" "fzf 0.74.3" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -408,9 +408,6 @@ Adaptive height has the following limitations:
|
|||||||
* Cannot be used with top/bottom margin and padding given in percent size
|
* Cannot be used with top/bottom margin and padding given in percent size
|
||||||
.br
|
.br
|
||||||
* It will not find the right size when there are multi-line items
|
* It will not find the right size when there are multi-line items
|
||||||
.br
|
|
||||||
* fzf cannot start until the input ends or exceeds the height, so a stream
|
|
||||||
that stays below it will delay the first render
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-min\-height=" "HEIGHT[+]"
|
.BI "\-\-min\-height=" "HEIGHT[+]"
|
||||||
|
|||||||
+12
-81
@@ -140,15 +140,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.
|
|
||||||
" Without a job, s:execute_tmux() blocks and freezes Vim. fzf also wants a
|
|
||||||
" tmux window of at least 3x3, and Vim's pane is never larger than the
|
|
||||||
" window, so asking Vim is free and survives a resize
|
|
||||||
if (has('nvim') || has('job')) && &columns >= 3 && &lines >= 3
|
|
||||||
\ && s:tmux_enabled() && get(s:, 'tmux_floating', 0)
|
|
||||||
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%' }
|
||||||
@@ -174,12 +165,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 = {}
|
||||||
@@ -272,7 +257,7 @@ function! fzf#exec(...)
|
|||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
" Path to the fzf-tmux script, or an empty string if it is not available. Only
|
" 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.
|
" the legacy options still need it. --tmux is handled by fzf itself.
|
||||||
function! s:fzf_tmux_script()
|
function! s:fzf_tmux_script()
|
||||||
if !executable(s:fzf_tmux)
|
if !executable(s:fzf_tmux)
|
||||||
if !executable('fzf-tmux')
|
if !executable('fzf-tmux')
|
||||||
@@ -288,7 +273,14 @@ function! s:tmux_enabled()
|
|||||||
return 0
|
return 0
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if empty($TMUX) && empty($ZELLIJ)
|
" --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
|
||||||
|
|
||||||
@@ -296,21 +288,7 @@ function! s:tmux_enabled()
|
|||||||
return s:tmux
|
return s:tmux
|
||||||
endif
|
endif
|
||||||
|
|
||||||
let [s:tmux, s:tmux_floating] = [0, 0]
|
let s:tmux = 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')
|
let output = system('tmux -V')
|
||||||
if v:shell_error
|
if v:shell_error
|
||||||
return s:tmux
|
return s:tmux
|
||||||
@@ -318,15 +296,9 @@ function! s:tmux_enabled()
|
|||||||
" e.g. 'tmux 3.7b', 'tmux next-3.8'
|
" e.g. 'tmux 3.7b', 'tmux next-3.8'
|
||||||
let ver = matchstr(output, '\d\+\.\d\+')
|
let ver = matchstr(output, '\d\+\.\d\+')
|
||||||
|
|
||||||
" --popup requires tmux 3.3 or above, and needs no fzf-tmux script. The
|
" --tmux requires tmux 3.3 or above, and needs no fzf-tmux script
|
||||||
" 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
|
if s:compare_versions(ver, '3.3') >= 0
|
||||||
let s:tmux = 1
|
let s:tmux = 1
|
||||||
let s:tmux_floating = s:compare_versions(ver, '3.7') >= 0
|
|
||||||
\ && s:tmux_floating_pane_support()
|
|
||||||
\ && s:compare_versions(s:get_version(s:fzf_binary()), '0.74.0') >= 0
|
|
||||||
return s:tmux
|
return s:tmux
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -479,10 +451,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
|
||||||
|
|
||||||
@@ -658,44 +627,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
|
|
||||||
|
|
||||||
" Whether the running server can put fzf in a floating pane. fzf decides on
|
|
||||||
" the 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, which also requires the tmux window to be at least 3x3; that
|
|
||||||
" one changes with a resize, so s:default_layout() reads it from Vim instead
|
|
||||||
" of caching it here
|
|
||||||
function! s:tmux_floating_pane_support()
|
|
||||||
" 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 list-commands new-pane')
|
|
||||||
" A server that does not know the command exits normally with no output
|
|
||||||
return !v:shell_error && out =~# 'new-pane'
|
|
||||||
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)
|
||||||
|
|||||||
@@ -124,8 +124,10 @@ function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion s
|
|||||||
|
|
||||||
# Determine the tabstop length for description alignment
|
# Determine the tabstop length for description alignment
|
||||||
set -l -- max_columns (math $COLUMNS - 40)
|
set -l -- max_columns (math $COLUMNS - 40)
|
||||||
for len in (string match -r -- '^[^\\t]*(?=\\t)' $list[1..500] | string length -V)
|
for i in $list[1..500]
|
||||||
test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
|
set -l -- item (string split -f 1 -- \t $i)
|
||||||
|
and set -l -- len (string length -V -- $item)
|
||||||
|
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
|
||||||
and set -- tabstop $len
|
and set -- tabstop $len
|
||||||
end
|
end
|
||||||
set -- tabstop (math $tabstop + 4)
|
set -- tabstop (math $tabstop + 4)
|
||||||
|
|||||||
+3
-10
@@ -104,7 +104,7 @@ function fzf_key_bindings
|
|||||||
# Store current token in $dir as root for the 'find' command
|
# Store current token in $dir as root for the 'find' command
|
||||||
function fzf-file-widget -d "List files and folders"
|
function fzf-file-widget -d "List files and folders"
|
||||||
set -l commandline (__fzf_parse_commandline)
|
set -l commandline (__fzf_parse_commandline)
|
||||||
set -l dir $commandline[1]
|
set -lx dir $commandline[1]
|
||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
set -l prefix $commandline[3]
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
@@ -112,17 +112,10 @@ function fzf_key_bindings
|
|||||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||||
"--multi $FZF_CTRL_T_OPTS --print0")
|
"--multi $FZF_CTRL_T_OPTS --print0")
|
||||||
|
|
||||||
|
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_COMMAND
|
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||||
|
|
||||||
if test -n "$FZF_CTRL_T_COMMAND"
|
|
||||||
set -f result (eval $FZF_CTRL_T_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
|
||||||
else
|
|
||||||
set -f result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
|
||||||
end
|
|
||||||
|
|
||||||
test -n "$result"
|
|
||||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
|
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
|
|||||||
+10
-34
@@ -47,20 +47,6 @@ def __fzfcmd []: nothing -> list<string> {
|
|||||||
['fzf']
|
['fzf']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keybinding modes to activate. The Helix modes only exist since Nushell
|
|
||||||
# 0.115.0, so they are only included when running a version that supports
|
|
||||||
# them (major > 0 covers a hypothetical 1.0+ where the minor resets).
|
|
||||||
def __fzf_modes []: nothing -> list<string> {
|
|
||||||
let v = version
|
|
||||||
let major = ($v.major | into int)
|
|
||||||
let minor = ($v.minor | into int)
|
|
||||||
if ($major > 0) or ($minor >= 115) {
|
|
||||||
['emacs', 'vi_normal', 'vi_insert', 'helix_normal', 'helix_select', 'helix_insert']
|
|
||||||
} else {
|
|
||||||
['emacs', 'vi_normal', 'vi_insert']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export-env {
|
export-env {
|
||||||
$env.FZF_CTRL_T_OPTS = $env.FZF_CTRL_T_OPTS? | default ""
|
$env.FZF_CTRL_T_OPTS = $env.FZF_CTRL_T_OPTS? | default ""
|
||||||
@@ -69,11 +55,11 @@ export-env {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
let alt_c = {
|
const alt_c = {
|
||||||
name: fzf_dirs
|
name: fzf_dirs
|
||||||
modifier: alt
|
modifier: alt
|
||||||
keycode: char_c
|
keycode: char_c
|
||||||
mode: (__fzf_modes)
|
mode: [emacs, vi_normal, vi_insert]
|
||||||
event: [
|
event: [
|
||||||
{
|
{
|
||||||
send: executehostcommand
|
send: executehostcommand
|
||||||
@@ -96,11 +82,11 @@ let alt_c = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# History
|
# History
|
||||||
let ctrl_r = {
|
const ctrl_r = {
|
||||||
name: fzf_history
|
name: fzf_history
|
||||||
modifier: control
|
modifier: control
|
||||||
keycode: char_r
|
keycode: char_r
|
||||||
mode: (__fzf_modes)
|
mode: [emacs, vi_insert, vi_normal]
|
||||||
event: [
|
event: [
|
||||||
{
|
{
|
||||||
send: executehostcommand
|
send: executehostcommand
|
||||||
@@ -129,16 +115,16 @@ let ctrl_r = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
let ctrl_t = {
|
const ctrl_t = {
|
||||||
name: fzf_files
|
name: fzf_files
|
||||||
modifier: control
|
modifier: control
|
||||||
keycode: char_t
|
keycode: char_t
|
||||||
mode: (__fzf_modes)
|
mode: [emacs, vi_normal, vi_insert]
|
||||||
event: [
|
event: [
|
||||||
{
|
{
|
||||||
send: executehostcommand
|
send: executehostcommand
|
||||||
cmd: "
|
cmd: "
|
||||||
let fzf_opts = (__fzf_defaults '--reverse --walker=file,dir,follow,hidden --scheme=path' $'($env.FZF_CTRL_T_OPTS) -m --print0');
|
let fzf_opts = (__fzf_defaults '--reverse --walker=file,dir,follow,hidden --scheme=path' $'($env.FZF_CTRL_T_OPTS) -m');
|
||||||
let fzfcmd = (__fzfcmd);
|
let fzfcmd = (__fzfcmd);
|
||||||
let fzf_args = ($fzfcmd | skip 1);
|
let fzf_args = ($fzfcmd | skip 1);
|
||||||
let ctrl_t_cmd = ($env.FZF_CTRL_T_COMMAND? | default null);
|
let ctrl_t_cmd = ($env.FZF_CTRL_T_COMMAND? | default null);
|
||||||
@@ -149,19 +135,9 @@ let ctrl_t = {
|
|||||||
let sh_cmd = [$ctrl_t_cmd '|' $fzf_cmd_str] | str join ' ';
|
let sh_cmd = [$ctrl_t_cmd '|' $fzf_cmd_str] | str join ' ';
|
||||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
||||||
};
|
};
|
||||||
# Serialize each path as a Nushell string literal, so that syntax
|
let result = ($result | str replace --all (char newline) ' ' | str trim);
|
||||||
# in a file name is not evaluated when the line is executed.
|
commandline edit --append $result;
|
||||||
let result = (
|
commandline set-cursor --end
|
||||||
$result
|
|
||||||
| split row (char nul)
|
|
||||||
| where {|path| $path != ''}
|
|
||||||
| each {|path| $path | to nuon}
|
|
||||||
| str join ' '
|
|
||||||
);
|
|
||||||
if ($result | is-not-empty) {
|
|
||||||
commandline edit --append $'($result) ';
|
|
||||||
commandline set-cursor --end
|
|
||||||
}
|
|
||||||
"
|
"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-15
@@ -1547,14 +1547,6 @@ func (t *Terminal) visibleInputLinesInList() int {
|
|||||||
|
|
||||||
// Extra number of lines needed to display fzf
|
// Extra number of lines needed to display fzf
|
||||||
func (t *Terminal) extraLines() int {
|
func (t *Terminal) extraLines() int {
|
||||||
// borderLines() reports zero for BorderInline, but addInline() still
|
|
||||||
// reserves a divider line for it
|
|
||||||
sectionLines := func(shape tui.BorderShape) int {
|
|
||||||
if shape == tui.BorderInline {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return borderLines(shape)
|
|
||||||
}
|
|
||||||
extra := 0
|
extra := 0
|
||||||
if !t.inputless {
|
if !t.inputless {
|
||||||
extra++
|
extra++
|
||||||
@@ -1570,16 +1562,16 @@ func (t *Terminal) extraLines() int {
|
|||||||
}
|
}
|
||||||
if t.headerVisible {
|
if t.headerVisible {
|
||||||
if t.hasHeaderWindow() {
|
if t.hasHeaderWindow() {
|
||||||
extra += sectionLines(t.headerBorderShape)
|
extra += borderLines(t.headerBorderShape)
|
||||||
}
|
}
|
||||||
extra += len(t.header0)
|
extra += len(t.header0)
|
||||||
if w, shape := t.determineHeaderLinesShape(); w {
|
if w, shape := t.determineHeaderLinesShape(); w {
|
||||||
extra += sectionLines(shape)
|
extra += borderLines(shape)
|
||||||
}
|
}
|
||||||
extra += t.headerLines
|
extra += t.headerLines
|
||||||
}
|
}
|
||||||
if len(t.footer) > 0 {
|
if len(t.footer) > 0 {
|
||||||
extra += sectionLines(t.footerBorderShape)
|
extra += borderLines(t.footerBorderShape)
|
||||||
extra += len(t.footer)
|
extra += len(t.footer)
|
||||||
}
|
}
|
||||||
return extra
|
return extra
|
||||||
@@ -2059,9 +2051,7 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
prevIndex = t.targetIndex
|
prevIndex = t.targetIndex
|
||||||
t.targetIndex = minItem.Index()
|
t.targetIndex = minItem.Index()
|
||||||
}
|
}
|
||||||
if result.final() {
|
t.progress = 100
|
||||||
t.progress = 100
|
|
||||||
}
|
|
||||||
t.merger = merger
|
t.merger = merger
|
||||||
t.resultMerger = merger
|
t.resultMerger = merger
|
||||||
t.passMerger = result.passMerger
|
t.passMerger = result.passMerger
|
||||||
@@ -8599,7 +8589,6 @@ func (t *Terminal) Loop() error {
|
|||||||
reload := changed || newCommand != nil
|
reload := changed || newCommand != nil
|
||||||
if reload {
|
if reload {
|
||||||
t.wait.searching = true
|
t.wait.searching = true
|
||||||
t.progress = 0
|
|
||||||
}
|
}
|
||||||
var reloadRequest *searchRequest
|
var reloadRequest *searchRequest
|
||||||
if reload {
|
if reload {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
+2
-53
@@ -26,7 +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
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultTtyDevice string = "/dev/tty"
|
const DefaultTtyDevice string = "/dev/tty"
|
||||||
@@ -175,7 +175,6 @@ type LightRenderer struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
yoffset int
|
yoffset int
|
||||||
xoffset int
|
|
||||||
tabstop int
|
tabstop int
|
||||||
escDelay int
|
escDelay int
|
||||||
fullscreen bool
|
fullscreen bool
|
||||||
@@ -278,7 +277,6 @@ func (r *LightRenderer) Init() error {
|
|||||||
// increased and we're left with unwanted extra new line.
|
// increased and we're left with unwanted extra new line.
|
||||||
if x > 0 && r.clearOnExit {
|
if x > 0 && r.clearOnExit {
|
||||||
r.upOneLine = true
|
r.upOneLine = true
|
||||||
r.xoffset = x
|
|
||||||
r.makeSpace()
|
r.makeSpace()
|
||||||
}
|
}
|
||||||
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
||||||
@@ -340,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)
|
||||||
}
|
}
|
||||||
@@ -419,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,
|
||||||
@@ -1180,13 +1132,10 @@ func (r *LightRenderer) Close() {
|
|||||||
r.rmcup()
|
r.rmcup()
|
||||||
} else {
|
} else {
|
||||||
r.origin()
|
r.origin()
|
||||||
// Erase our own area first, then step back onto the line the
|
|
||||||
// prompt was on, so nothing on that line is touched
|
|
||||||
r.csi("J")
|
|
||||||
if r.upOneLine {
|
if r.upOneLine {
|
||||||
r.csi("A")
|
r.csi("A")
|
||||||
r.csi(fmt.Sprintf("%dG", r.xoffset+1))
|
|
||||||
}
|
}
|
||||||
|
r.csi("J")
|
||||||
}
|
}
|
||||||
} else if !r.fullscreen {
|
} else if !r.fullscreen {
|
||||||
r.stderr("\x1b8") // DECRC: restore cursor position
|
r.stderr("\x1b8") // DECRC: restore cursor position
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -240,7 +240,7 @@ func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResu
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
cancelFd := int(rpipe.Fd())
|
cancelFd := int(rpipe.Fd())
|
||||||
for {
|
for range maxSelectTries {
|
||||||
var rfds unix.FdSet
|
var rfds unix.FdSet
|
||||||
limit := len(rfds.Bits) * unix.NFDBITS
|
limit := len(rfds.Bits) * unix.NFDBITS
|
||||||
if fd >= limit || cancelFd >= limit {
|
if fd >= limit || cancelFd >= limit {
|
||||||
@@ -251,8 +251,6 @@ func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResu
|
|||||||
rfds.Set(cancelFd)
|
rfds.Set(cancelFd)
|
||||||
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
|
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// An interrupted wait is not a failed read, so it must not count
|
|
||||||
// against anything. Retry until the fd is ready or the wait fails.
|
|
||||||
if err == syscall.EINTR {
|
if err == syscall.EINTR {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -267,6 +265,7 @@ func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResu
|
|||||||
return getter()
|
return getter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0, getCharError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) Size() TermSize {
|
func (r *LightRenderer) Size() TermSize {
|
||||||
|
|||||||
@@ -882,28 +882,6 @@ class TestLayout < TestInteractive
|
|||||||
tmux.until { assert_block(block, it) }
|
tmux.until { assert_block(block, it) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_adaptive_height_with_inline_sections
|
|
||||||
tmux.send_keys %(seq 10 | #{FZF} --height=~100% --list-border --header-lines=1 --header-lines-border=inline), :Enter
|
|
||||||
block = <<~BLOCK
|
|
||||||
╭──────
|
|
||||||
│ 10
|
|
||||||
│ 9
|
|
||||||
│ 8
|
|
||||||
│ 7
|
|
||||||
│ 6
|
|
||||||
│ 5
|
|
||||||
│ 4
|
|
||||||
│ 3
|
|
||||||
│ > 2
|
|
||||||
├──────
|
|
||||||
│ 1
|
|
||||||
╰──────
|
|
||||||
9/9
|
|
||||||
>
|
|
||||||
BLOCK
|
|
||||||
tmux.until { assert_block(block, it) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_style_full_adaptive_height
|
def test_style_full_adaptive_height
|
||||||
tmux.send_keys %(seq 1| #{FZF} --style=full:rounded --height=~100% --header-lines=1 --info=default), :Enter
|
tmux.send_keys %(seq 1| #{FZF} --style=full:rounded --height=~100% --header-lines=1 --info=default), :Enter
|
||||||
block = <<~BLOCK
|
block = <<~BLOCK
|
||||||
|
|||||||
@@ -55,29 +55,6 @@ class TestServer < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_listen_progress
|
|
||||||
tmux.send_keys "seq 10 | #{FZF} --listen 6266", :Enter
|
|
||||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
|
||||||
uri = URI('http://localhost:6266')
|
|
||||||
state = -> { JSON.parse(Net::HTTP.get(uri), symbolize_names: true) }
|
|
||||||
|
|
||||||
# Idle: the last search is complete
|
|
||||||
assert_equal 100, state.call[:progress]
|
|
||||||
|
|
||||||
# While a search is running, progress is not left at 100 from the
|
|
||||||
# previous one, so it can tell a settled snapshot from a stale one
|
|
||||||
Net::HTTP.post(uri, 'reload(sleep 1; seq 100)')
|
|
||||||
tmux.until { assert_equal 0, state.call[:progress] }
|
|
||||||
|
|
||||||
# Settled: matches belong to the snapshot that reports 100
|
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
|
||||||
tmux.until do
|
|
||||||
st = state.call
|
|
||||||
assert_equal 100, st[:progress]
|
|
||||||
assert_equal 100, st[:matchCount]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_listen_with_api_key
|
def test_listen_with_api_key
|
||||||
uri = URI('http://localhost:6266')
|
uri = URI('http://localhost:6266')
|
||||||
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||||
|
|||||||
@@ -1273,43 +1273,6 @@ class TestNushell < TestBase
|
|||||||
FileUtils.rm_rf('/tmp/fzf-test')
|
FileUtils.rm_rf('/tmp/fzf-test')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Override: paths are inserted as Nushell string literals, so the
|
|
||||||
# selections appear quoted on the command line.
|
|
||||||
def test_ctrl_t
|
|
||||||
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
|
||||||
|
|
||||||
tmux.prepare
|
|
||||||
tmux.send_keys 'C-t'
|
|
||||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
|
||||||
tmux.send_keys :Tab, :Tab, :Tab
|
|
||||||
tmux.until { |lines| assert lines.any_include?(' (3)') }
|
|
||||||
tmux.send_keys :Enter
|
|
||||||
tmux.until { |lines| assert lines.any_include?('"1" "2" "3"') }
|
|
||||||
tmux.send_keys 'C-c'
|
|
||||||
end
|
|
||||||
|
|
||||||
# A path is inserted as a Nushell string literal, so that syntax in a file
|
|
||||||
# name is not evaluated and each path stays a single argument.
|
|
||||||
def test_ctrl_t_quoting
|
|
||||||
marker = "#{tempname}-marker"
|
|
||||||
FileUtils.rm_f(marker)
|
|
||||||
writelines(["fzf-inject$(touch #{marker}).txt", 'fzf-inject space.txt'])
|
|
||||||
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
|
|
||||||
|
|
||||||
tmux.prepare
|
|
||||||
tmux.send_keys '^printf "%s\n" ', 'C-t'
|
|
||||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
|
||||||
tmux.send_keys :Tab, :Tab
|
|
||||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
|
||||||
tmux.send_keys :Enter
|
|
||||||
tmux.until { |lines| assert_includes lines[-1].to_s, '"fzf-inject$(touch' }
|
|
||||||
tmux.send_keys :Enter
|
|
||||||
tmux.until do |lines|
|
|
||||||
assert_equal ["fzf-inject$(touch #{marker}).txt", 'fzf-inject space.txt'], lines[-2..]
|
|
||||||
end
|
|
||||||
refute_path_exists marker
|
|
||||||
end
|
|
||||||
|
|
||||||
# Nushell does not support multiline command recall the same way
|
# Nushell does not support multiline command recall the same way
|
||||||
# as bash/zsh/fish, so test_ctrl_r_multiline is omitted.
|
# as bash/zsh/fish, so test_ctrl_r_multiline is omitted.
|
||||||
|
|
||||||
|
|||||||
@@ -102,36 +102,9 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
|
|||||||
" 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
|
||||||
|
|||||||
Reference in New Issue
Block a user