Compare commits

..

30 Commits

Author SHA1 Message Date
Junegunn Choi
64c61603e9 0.56.1 2024-11-10 23:15:35 +09:00
LangLangBart
57c08d925f Enhance command extraction in zsh completion (#4082)
Fix #1992
2024-11-10 17:40:59 +09:00
junegunn
51623a5f6a Deploying to master from @ junegunn/fzf@ca3f6181d7 🚀 2024-11-10 00:02:06 +00:00
Junegunn Choi
ca3f6181d7 page-up/down: undo last up/down if items are skipped
Fix #4069
2024-11-09 11:54:41 +09:00
Junegunn Choi
9c94f9c3d0 Another attempt to fix (half-)page-up/down for multi-line items
Fix #4069
2024-11-08 20:18:42 +09:00
Junegunn Choi
4a85843bcf Fix (half-)page-up/down in the presence of multi-line items
Fix #4069
2024-11-07 22:21:07 +09:00
jaydee-coder
d4d9b99879 Allow specifying '{n}' as the OFFSET in the preview-window flag (#4079)
* fzf: Allow '{n}' to be used as the OFFSET in the preview-window flag

* man: Document using '{n}' as the OFFSET in the preview-window flag
2024-11-04 22:27:59 +09:00
jaydee-coder
6816b7d95b docker: fix dockerfile warnings (#4080)
The following warnings were emitted when running `make docker-test`:
```
 2 warnings found (use docker --debug to expand):
 - LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format (line 11)
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 12)
```

This change fixes both of these 2 trivial warnings.
2024-11-04 22:26:19 +09:00
Junegunn Choi
acdf265d7a Fix reader regression (#4070) 2024-11-03 20:32:26 +09:00
Junegunn Choi
19495eb9bb Remove possible races (#4070) 2024-11-03 20:12:47 +09:00
Junegunn Choi
bacc8609ee Fix characters from previous preview not being cleared
Fix #4075
2024-11-03 15:07:17 +09:00
junegunn
99163f5afa Deploying to master from @ junegunn/fzf@0607227730 🚀 2024-11-03 00:02:14 +00:00
LangLangBart
0607227730 fix(zsh): move 'fc -RI' inside command substitution (#4073)
* fix(zsh): move 'fc -RI' inside command substitution

* chore: follow existing option check format
2024-11-02 10:41:17 +09:00
LangLangBart
d938fdc496 fix(zsh): history loading with shared option (#4071)
Fix #4061
2024-11-01 00:19:47 +09:00
Junegunn Choi
dcb4c3d84a Fix race in reload action
Fix #4070
2024-10-31 19:40:40 +09:00
Junegunn Choi
82ebcd9209 Fix (half-)page-up/down in the presence of multi-line items
Fix #4069
2024-10-30 16:52:42 +09:00
Junegunn Choi
ff1687744d 0.56.0 2024-10-27 12:03:01 +09:00
junegunn
782c870fb2 Deploying to master from @ junegunn/fzf@71fad63829 🚀 2024-10-27 00:02:14 +00:00
Charlie Vieth
71fad63829 Update fastwalk to v1.0.9 to fix handling of disk root paths on Windows (#4063)
Fixes: https://github.com/junegunn/fzf/issues/4027
2024-10-25 23:57:46 +09:00
Junegunn Choi
d65c6101a8 walker: Do not treat '..' as a hidden entry
Thanks to @LangLangBart for the suggested fix

Fix #4048
2024-10-25 13:50:58 +09:00
junegunn
3c40b1bd51 Deploying to master from @ junegunn/fzf@90a8800bb5 🚀 2024-10-20 00:02:15 +00:00
Junegunn Choi
90a8800bb5 Avoid selecting an outdated merger from cache
We cache a merger for partial input as well, because it is automatically
invalidated as soon as the new data comes in.

However, there was a race condition where a cached merger for a partial
input is used even after the input stream was complete. This commit
fixes the problem.

Fix #4034
2024-10-16 00:45:12 +09:00
Thomas Martitz
97f1dae2d1 Use eval to evaluate $post variable as command. (#4023) 2024-10-15 18:00:27 +09:00
dependabot[bot]
e54ec05709 Bump crate-ci/typos from 1.24.1 to 1.26.0 (#4036)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.24.1 to 1.26.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.24.1...v1.26.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 17:38:14 +09:00
Junegunn Choi
a24eb99679 Fix full line background in preview window 2024-10-15 17:35:11 +09:00
dependabot[bot]
ad113d00b7 Bump golang.org/x/term from 0.24.0 to 0.25.0 (#4031)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/term/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 16:56:37 +09:00
junegunn
7bd5884d12 Deploying to master from @ junegunn/fzf@c3505858a6 🚀 2024-10-13 00:02:13 +00:00
junegunn
c3505858a6 Deploying to master from @ junegunn/fzf@e76aa37fd4 🚀 2024-10-06 00:02:11 +00:00
Junegunn Choi
e76aa37fd4 Make RuboCop happy 2024-10-01 19:45:53 +09:00
Junegunn Choi
1a32220ca9 Add --gap option to put empty lines between items 2024-10-01 19:15:17 +09:00
22 changed files with 327 additions and 156 deletions

View File

@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@v1.24.1
- uses: crate-ci/typos@v1.26.0

View File

@@ -1,6 +1,34 @@
CHANGELOG
=========
0.56.1
------
- Bug fixes and improvements
- Fixed a race condition which would cause fzf to present stale results after `reload` (#4070)
- `page-up` and `page-down` actions now work correctly with multi-line items (#4069)
- `{n}` is allowed in `SCROLL` expression in `--preview-window` (#4079)
- [zsh] Fixed regression in history loading with shared option (#4071)
- [zsh] Better command extraction in zsh completion (#4082)
- Thanks to @LangLangBart, @jaydee-coder, @alex-huff, and @vejkse for the contributions
0.56.0
------
- Added `--gap[=N]` option to display empty lines between items.
- This can be useful to visually separate adjacent multi-line items.
```sh
# All bash functions, highlighted
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi --highlight-line --gap
```
- Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
```sh
fzf --info inline-right --gap --color gutter:-1
```
- Added `noinfo` option to `--preview-window` to hide the scroll indicator in the preview window
- Bug fixes
- Thanks to @LangLangBart, @akinomyoga, and @charlievieth for fixing the bugs
0.55.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_

View File

@@ -8,5 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
RUN rm -f /etc/bash.bashrc
COPY . /fzf
RUN cd /fzf && make install && ./install --all
ENV LANG C.UTF-8
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
ENV LANG=C.UTF-8
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]

File diff suppressed because one or more lines are too long

6
go.mod
View File

@@ -1,13 +1,13 @@
module github.com/junegunn/fzf
require (
github.com/charlievieth/fastwalk v1.0.8
github.com/charlievieth/fastwalk v1.0.9
github.com/gdamore/tcell/v2 v2.7.4
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.25.0
golang.org/x/term v0.24.0
golang.org/x/sys v0.26.0
golang.org/x/term v0.25.0
)
require (

12
go.sum
View File

@@ -1,5 +1,5 @@
github.com/charlievieth/fastwalk v1.0.8 h1:uaoH6cAKSk73aK7aKXqs0+bL+J3Txzd3NGH8tRXgHko=
github.com/charlievieth/fastwalk v1.0.8/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u
version=0.55.0
version=0.56.1
auto_completion=
key_bindings=
update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.55.0"
$version="0.56.1"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.55"
var version = "0.56"
var revision = "devel"
//go:embed shell/key-bindings.bash

View File

@@ -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
THE SOFTWARE.
..
.TH fzf\-tmux 1 "Aug 2024" "fzf 0.55.0" "fzf\-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "Nov 2024" "fzf 0.56.1" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf\-tmux - open fzf in tmux split pane

View File

@@ -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
THE SOFTWARE.
..
.TH fzf 1 "Sep 2024" "fzf 0.56.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Nov 2024" "fzf 0.56.1" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -208,6 +208,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
.B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR
.TP
.BI "\-\-gap" "[=N]"
Render empty lines between each item
.TP
.B "\-\-keep\-right"
Keep the right end of the line visible when it's too long. Effective only when
the query string is empty.
@@ -802,7 +805,7 @@ e.g. \fBborder\-rounded\fR (border with rounded edges, default),
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
preview window.
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer or {n} to refer to the zero-based ordinal index of the current item.
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB\-INTEGER\fR or \fB+INTEGER\fR).

View File

@@ -283,7 +283,7 @@ _fzf_handle_dynamic_completion() {
}
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd rest
local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]}"
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
@@ -295,16 +295,6 @@ __fzf_generic_path_completion() {
base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" 2> /dev/null || return
# Try to leverage existing completion
rest=("${@:4}")
unset 'rest[${#rest[@]}-2]'
COMP_LINE=${COMP_LINE:0:${#COMP_LINE}-${#trigger}}
COMP_POINT=$((COMP_POINT-${#trigger}))
COMP_WORDS[$COMP_CWORD]=$base
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
[[ $? -ne 0 ]] &&
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
dir=
[[ $base = *"/"* ]] && dir="$base"
while true; do
@@ -316,11 +306,7 @@ __fzf_generic_path_completion() {
matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if [[ ${#COMPREPLY[@]} -gt 0 ]]; then
for h in "${COMPREPLY[@]}"; do
echo "$h"
done | command sort -u | __fzf_comprun "$4" -q "$leftover"
elif declare -F "$1" > /dev/null; then
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
if [[ $1 =~ dir ]]; then
@@ -388,23 +374,10 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}}
# Try to leverage existing completion
COMP_LINE=${COMP_LINE:0:${#COMP_LINE}-${#trigger}}
COMP_POINT=$((COMP_POINT-${#trigger}))
unset 'rest[${#rest[@]}-2]'
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
[[ $? -ne 0 ]] &&
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
selected=$(
(if [[ ${#COMPREPLY[@]} -gt 0 ]]; then
for h in "${COMPREPLY[@]}"; do
echo "$h"
done
fi; cat) | command sort -u |
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")

View File

@@ -120,25 +120,19 @@ __fzf_comprun() {
fi
}
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
__fzf_extract_command() {
local token tokens
tokens=(${(z)1})
for token in $tokens; do
token=${(Q)token}
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
echo "$token"
return
fi
done
echo "${tokens[1]}"
setopt localoptions noksh_arrays
# Control completion with the "compstate" parameter, insert and list noting
compstate[insert]=
compstate[list]=
cmd_word="${words[1]}"
}
__fzf_generic_path_completion() {
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
local base lbuf compgen fzf_opts suffix tail dir leftover matches
base=$1
lbuf=$2
cmd=$(__fzf_extract_command "$lbuf")
compgen=$3
fzf_opts=$4
suffix=$5
@@ -161,7 +155,7 @@ __fzf_generic_path_completion() {
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
if [[ $compgen =~ dir ]]; then
walker=dir,follow
@@ -170,7 +164,7 @@ __fzf_generic_path_completion() {
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read -r item; do
item="${item%$suffix}$suffix"
echo -n -E "${(q)item} "
@@ -227,10 +221,9 @@ _fzf_complete() {
rest=("$@")
fi
local fifo lbuf cmd matches post
local fifo lbuf matches post
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
lbuf=${rest[0]}
cmd=$(__fzf_extract_command "$lbuf")
post="${funcstack[1]}_post"
type $post > /dev/null 2>&1 || post=cat
@@ -238,7 +231,7 @@ _fzf_complete() {
matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
@@ -310,9 +303,16 @@ _fzf_complete_kill_post() {
}
fzf-completion() {
local tokens cmd prefix trigger tail matches lbuf d_cmds
trap 'unset cmd_word' EXIT
local tokens prefix trigger tail matches lbuf d_cmds
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# Check if at least one completion system (old or new) is active
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( $+functions[compdef] )); then
if ! zmodload -e zsh/compctl; then
zmodload -i zsh/compctl
fi
fi
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER})
@@ -321,8 +321,6 @@ fzf-completion() {
return
fi
cmd=$(__fzf_extract_command "$LBUFFER")
# Explicitly allow for empty trigger.
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
@@ -340,16 +338,20 @@ fzf-completion() {
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
# Make the 'cmd_word' global
zle __fzf_extract_command || :
[[ -z "$cmd_word" ]] && return
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
return
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
if eval "type _fzf_complete_${cmd_word} > /dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
zle reset-prompt
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" "$lbuf"
else
_fzf_path_completion "$prefix" "$lbuf"
@@ -366,6 +368,9 @@ fzf-completion() {
unset binding
}
# Completion widget to gain access to the 'words' array (man zshcompwid)
zle -C __fzf_extract_command .complete-word __fzf_extract_command
# Normal widget
zle -N fzf-completion
bindkey '^I' fzf-completion
fi

View File

@@ -108,13 +108,20 @@ fi
fzf-history-widget() {
local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
# Ensure the associative history array, which maps event numbers to the full
# history lines, is loaded, and that Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
# Ensure the module is loaded if not already, and the required features, such
# as the associative 'history' array, which maps event numbers to full history
# lines, are set. Also, make sure Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( $+commands[perl] )); then
# Import commands from other shells if SHARE_HISTORY is enabled, as the
# 'history' array only updates after executing a non-empty command.
selected="$(
if [[ -o sharehistory ]]; then
fc -RI
fi
printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
else
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \

View File

@@ -172,7 +172,9 @@ func Run(opts *Options) (int, error) {
return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
readyChan := make(chan bool)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
<-readyChan
}
// Matcher
@@ -224,7 +226,7 @@ func Run(opts *Options) (int, error) {
}
return false
}, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
@@ -299,7 +301,9 @@ func Run(opts *Options) (int, error) {
itemIndex = 0
inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ)
readyChan := make(chan bool)
go reader.restart(command, environ, readyChan)
<-readyChan
}
exitCode := ExitOk

View File

@@ -102,7 +102,7 @@ func (m *Matcher) Loop() {
if !cacheCleared {
if count == prevCount {
// Look up mergerCache
if cached, found := m.mergerCache[patternString]; found {
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
merger = cached
}
} else {

View File

@@ -56,6 +56,7 @@ Usage: fzf [options]
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0
--gap[=N] Render empty lines between each item
--keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0)
@@ -473,6 +474,7 @@ type Options struct {
Header []string
HeaderLines int
HeaderFirst bool
Gap int
Ellipsis *string
Scrollbar *string
Margin [4]sizeSpec
@@ -579,6 +581,7 @@ func defaultOptions() *Options {
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
Gap: 0,
Ellipsis: nil,
Scrollbar: nil,
Margin: defaultMargin(),
@@ -1723,7 +1726,7 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
var err error
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
offsetRegex := regexp.MustCompile(`^(\+{(-?[0-9]+|n)})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
tokens := tokenRegex.FindAllStringSubmatch(input, -1)
var alternative string
@@ -2343,6 +2346,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--gap":
if opts.Gap, err = optionalNumeric(allArgs, &i, 1); err != nil {
return err
}
case "--no-gap":
opts.Gap = 0
case "--ellipsis":
str, err := nextString(allArgs, &i, "ellipsis string required")
if err != nil {
@@ -2630,6 +2639,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.HeaderLines, err = atoi(value); err != nil {
return err
}
} else if match, value := optString(arg, "--gap="); match {
if opts.Gap, err = atoi(value); err != nil {
return err
}
} else if match, value := optString(arg, "--ellipsis="); match {
str := firstLine(value)
opts.Ellipsis = &str

View File

@@ -6,7 +6,6 @@ import (
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sync"
"sync/atomic"
@@ -25,16 +24,26 @@ type Reader struct {
event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
execOut io.ReadCloser
command *string
killed bool
termFunc func()
command *string
wait bool
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
return &Reader{
pusher,
executor,
eventBox,
delimNil,
int32(EvtReady),
make(chan bool, 1),
sync.Mutex{},
false,
func() { os.Stdin.Close() },
nil,
wait}
}
func (r *Reader) startEventPoller() {
@@ -80,19 +89,19 @@ func (r *Reader) fin(success bool) {
func (r *Reader) terminate() {
r.mutex.Lock()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
if r.termFunc != nil {
r.termFunc()
r.termFunc = nil
}
r.mutex.Unlock()
}
func (r *Reader) restart(command commandSpec, environ []string) {
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(command.command, environ)
success := r.readFromCommand(command.command, environ, func() {
readyChan <- true
})
r.fin(success)
removeFiles(command.tempFiles)
}
@@ -111,21 +120,29 @@ func (r *Reader) readChannel(inputChan chan string) bool {
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string) {
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
r.startEventPoller()
var success bool
signalReady := func() {
if readyChan != nil {
readyChan <- true
}
}
if inputChan != nil {
signalReady()
success = r.readChannel(inputChan)
} else if len(initCmd) > 0 {
success = r.readFromCommand(initCmd, initEnv)
success = r.readFromCommand(initCmd, initEnv, signalReady)
} else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
signalReady()
success = r.readFiles(root, opts, ignores)
} else {
success = r.readFromCommand(cmd, initEnv)
success = r.readFromCommand(cmd, initEnv, signalReady)
}
} else {
signalReady()
success = r.readFromStdin()
}
r.fin(success)
@@ -249,7 +266,6 @@ func trimPath(path string) string {
}
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
r.killed = false
conf := fastwalk.Config{
Follow: opts.follow,
// Use forward slashes when running a Windows binary under WSL or MSYS
@@ -265,7 +281,7 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
isDir := de.IsDir()
if isDir || opts.follow && isSymlinkToDir(path, de) {
base := filepath.Base(path)
if !opts.hidden && base[0] == '.' {
if !opts.hidden && base[0] == '.' && base != ".." {
return filepath.SkipDir
}
for _, ignore := range ignores {
@@ -288,31 +304,32 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
return fastwalk.Walk(&conf, root, fn) == nil
}
func (r *Reader) readFromCommand(command string, environ []string) bool {
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
r.mutex.Lock()
r.killed = false
r.termFunc = nil
r.command = &command
r.exec = r.executor.ExecCommand(command, true)
exec := r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
exec.Env = environ
}
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil {
r.exec = nil
execOut, err := exec.StdoutPipe()
if err != nil || exec.Start() != nil {
signalReady()
r.mutex.Unlock()
return false
}
err = r.exec.Start()
if err != nil {
r.exec = nil
r.mutex.Unlock()
return false
// Function to call to terminate the running command
r.termFunc = func() {
execOut.Close()
util.KillCommand(exec)
}
signalReady()
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil
r.feed(execOut)
return exec.Wait() == nil
}

View File

@@ -23,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
}
// Normal command
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
counter := 0
ready := func() {
counter++
}
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
t.Errorf("%s", strs)
}
@@ -48,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
reader.fin(reader.readFromCommand(`no-such-command`, nil))
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
strs = []string{}
if len(strs) > 0 {
if len(strs) > 0 || counter != 2 {
t.Errorf("%s", strs)
}

View File

@@ -245,6 +245,7 @@ type Terminal struct {
hscroll bool
hscrollOff int
scrollOff int
gap int
wordRubout string
wordNext string
cx int
@@ -825,6 +826,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
headerVisible: true,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
gap: opts.Gap,
header: []string{},
header0: opts.Header,
ansi: opts.Ansi,
@@ -1136,15 +1138,23 @@ func (t *Terminal) wrapCols() int {
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
}
// Number of lines the item takes including the gap
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
var numLines int
if !t.wrap && !t.multiLine {
return 1, false
numLines = 1 + t.gap
return numLines, numLines > atMost
}
var overflow bool
if !t.wrap && t.multiLine {
return item.text.NumLines(atMost)
numLines, overflow = item.text.NumLines(atMost)
} else {
var lines [][]rune
lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
numLines = len(lines)
}
lines, overflow := item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
return len(lines), overflow
numLines += t.gap
return numLines, overflow || numLines > atMost
}
func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
@@ -2050,6 +2060,21 @@ func (t *Terminal) printHeader() {
t.wrap = wrap
}
func (t *Terminal) canSpanMultiLines() bool {
return t.multiLine || t.wrap || t.gap > 0
}
func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
t.move(line, 0, true)
t.markEmptyLine(line)
// If the screen is not filled with the list in non-multi-line mode,
// scrollbar is not visible at all. But in multi-line mode, we may need
// to redraw the scrollbar character at the end.
if t.canSpanMultiLines() {
t.prevLines[line].hasBar = t.printBar(line, true, barRange)
}
}
func (t *Terminal) printList() {
t.constrain()
barLength, barStart := t.getScrollbar()
@@ -2070,14 +2095,7 @@ func (t *Terminal) printList() {
item := t.merger.Get(itemCount + t.offset)
line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
} else if !t.prevLines[line].empty {
t.move(line, 0, true)
t.markEmptyLine(line)
// If the screen is not filled with the list in non-multi-line mode,
// scrollbar is not visible at all. But in multi-line mode, we may need
// to redraw the scrollbar character at the end.
if t.multiLine || t.wrap {
t.prevLines[line].hasBar = t.printBar(line, true, barRange)
}
t.renderEmptyLine(line, barRange)
}
}
}
@@ -2125,9 +2143,6 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
t.prevLines[line].hasBar = printBar(line, false)
if !t.multiLine && !t.wrap {
return line
}
return line + numLines - 1
}
@@ -2214,6 +2229,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
}
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
finalLineNum++
t.renderEmptyLine(finalLineNum, barRange)
}
return finalLineNum
}
@@ -2275,7 +2294,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxLines := 1
if t.multiLine || t.wrap {
if t.canSpanMultiLines() {
maxLines = maxLineNum - lineNum + 1
}
lines, overflow := t.itemLines(item, maxLines)
@@ -2285,7 +2304,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
topCutoff := false
skipLines := 0
wrapped := false
if t.multiLine || t.wrap {
if t.canSpanMultiLines() {
// Cut off the upper lines in the 'default' layout
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
lines, _ = t.itemLines(item, math.MaxInt)
@@ -2702,11 +2721,15 @@ Loop:
url = nil
t.pwindow.LinkEnd()
}
if ansi != nil {
lbg = ansi.lbg
} else {
lbg = -1
}
str, width := t.processTabs(trimmed, prefixWidth)
if width > prefixWidth {
prefixWidth = width
if t.theme.Colored && ansi != nil && ansi.colored() {
lbg = ansi.lbg
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
@@ -2728,7 +2751,7 @@ Loop:
if unchanged && lineNo == 0 {
break
}
if lbg >= 0 {
if t.theme.Colored && lbg >= 0 {
fillRet = t.pwindow.CFill(-1, lbg, tui.AttrRegular,
strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
} else {
@@ -4387,17 +4410,77 @@ func (t *Terminal) Loop() error {
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems()-1, false)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1), false)
req(reqList)
case actHalfPageUp:
t.vmove(t.maxItems()/2, false)
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2), false)
case actPageUp, actPageDown, actHalfPageUp, actHalfPageDown:
// Calculate the number of lines to move
maxItems := t.maxItems()
linesToMove := maxItems - 1
if a.t == actHalfPageUp || a.t == actHalfPageDown {
linesToMove = maxItems / 2
}
// Move at least one line even in a very short window
linesToMove = util.Max(1, linesToMove)
// Determine the direction of the movement
direction := -1
if a.t == actPageUp || a.t == actHalfPageUp {
direction = 1
}
// In non-default layout, items are listed from top to bottom
if t.layout != layoutDefault {
direction *= -1
}
// We can simply add the number of lines to the current position in
// single-line mode
if !t.canSpanMultiLines() {
t.vset(t.cy + direction*linesToMove)
req(reqList)
break
}
// But in multi-line mode, we need to carefully limit the amount of
// vertical movement so that items are not skipped. In order to do
// this, we calculate the minimum or maximum offset based on the
// direction of the movement and the number of lines of the items
// around the current scroll offset.
var minOffset, maxOffset, lineSum int
if direction > 0 {
maxOffset = t.offset
for ; maxOffset < t.merger.Length(); maxOffset++ {
itemLines, _ := t.numItemLines(t.merger.Get(maxOffset).item, maxItems)
lineSum += itemLines
if lineSum >= maxItems {
break
}
}
} else {
minOffset = t.offset
for ; minOffset >= 0 && minOffset < t.merger.Length(); minOffset-- {
itemLines, _ := t.numItemLines(t.merger.Get(minOffset).item, maxItems)
lineSum += itemLines
if lineSum >= maxItems {
if lineSum > maxItems {
minOffset++
}
break
}
}
}
for i := 0; i < linesToMove; i++ {
cy, offset := t.cy, t.offset
t.vset(cy + direction)
t.constrain()
if cy == t.cy {
break
}
if i > 0 && (direction > 0 && t.offset > maxOffset ||
direction < 0 && t.offset < minOffset) {
t.cy, t.offset = cy, offset
break
}
}
req(reqList)
case actOffsetUp, actOffsetDown:
diff := 1
@@ -4875,7 +4958,7 @@ func (t *Terminal) constrain() {
for tries := 0; tries < maxLines; tries++ {
numItems := maxLines
// How many items can be fit on screen including the current item?
if (t.multiLine || t.wrap) && t.merger.Length() > 0 {
if t.canSpanMultiLines() && t.merger.Length() > 0 {
numItemsFound := 0
linesSum := 0
@@ -4930,12 +5013,12 @@ func (t *Terminal) constrain() {
for {
prevOffset := newOffset
numItems := t.merger.Length()
itemLines := 1
if (t.multiLine || t.wrap) && t.cy < numItems {
itemLines := 1 + t.gap
if t.canSpanMultiLines() && t.cy < numItems {
itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
}
linesBefore := t.cy - newOffset
if t.multiLine || t.wrap {
if t.canSpanMultiLines() {
linesBefore = 0
for i := newOffset; i < t.cy && i < numItems; i++ {
lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)

View File

@@ -1097,7 +1097,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
}
}
}
if w.posx+1 >= w.Width() {
if w.posx >= w.Width() {
if w.posy+1 >= w.height {
return FillSuspend
}

View File

@@ -3392,6 +3392,40 @@ class TestGoFZF < TestBase
assert lines[1]&.end_with?('1000││')
end
end
def test_gap
tmux.send_keys %(seq 100 | #{FZF} --gap --border --reverse), :Enter
block = <<~BLOCK
>
100/100
> 1
2
3
4
BLOCK
tmux.until { assert_block(block, _1) }
end
def test_gap_2
tmux.send_keys %(seq 100 | #{FZF} --gap=2 --border --reverse), :Enter
block = <<~BLOCK
>
100/100
> 1
2
3
BLOCK
tmux.until { assert_block(block, _1) }
end
end
module TestShell