Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] f21ea8163f Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 13:16:18 +00:00
76 changed files with 1310 additions and 4072 deletions
-64
View File
@@ -1,64 +0,0 @@
go:
- changed-files:
- any-glob-to-any-file:
- src/**
- main.go
- go.mod
- go.sum
shell:
- changed-files:
- any-glob-to-any-file:
- shell/**
bash:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.bash
zsh:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.zsh
fish:
- changed-files:
- any-glob-to-any-file:
- shell/**/*.fish
vim:
- changed-files:
- any-glob-to-any-file:
- plugin/**
docs:
- changed-files:
- any-glob-to-any-file:
- '*.md'
- doc/**
- man/**
ci:
- changed-files:
- any-glob-to-any-file:
- .github/**
build:
- changed-files:
- any-glob-to-any-file:
- Makefile
- .goreleaser.yml
- Dockerfile
test:
- changed-files:
- any-glob-to-any-file:
- test/**
- src/**/*_test.go
install:
- changed-files:
- any-glob-to-any-file:
- install
- install.ps1
- uninstall
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
-17
View File
@@ -1,17 +0,0 @@
name: Label PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
configuration-path: .github/labeler.yml
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
+2 -2
View File
@@ -3,13 +3,13 @@ name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 0 15 * * 6
- cron: 0 0 * * 0
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1
+1 -1
View File
@@ -6,5 +6,5 @@ jobs:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1.29.4
+4 -6
View File
@@ -85,14 +85,12 @@ notarize:
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
ids:
builds:
- fzf
formats:
- tar.gz
format: tar.gz
format_overrides:
- goos: windows
formats:
- zip
format: zip
files:
- non-existent*
@@ -104,7 +102,7 @@ release:
name_template: '{{ .Version }}'
snapshot:
version_template: "{{ .Version }}-devel"
name_template: "{{ .Version }}-devel"
changelog:
sort: asc
+1 -1
View File
@@ -363,7 +363,7 @@ projects, and it will free up memory as you narrow down the results.
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q} || true" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
-55
View File
@@ -1,61 +1,6 @@
CHANGELOG
=========
0.68.0
------
- Implemented word wrapping in the list section
- Added `--wrap=word` (or `--wrap-word`) option and `toggle-wrap-word` action for word-level line wrapping in the list section
- Changed default binding of `ctrl-/` and `alt-/` from `toggle-wrap` to `toggle-wrap-word`
```sh
fzf --wrap=word
```
- Implemented word wrapping in the preview window
- Added `wrap-word` flag for `--preview-window` to enable word-level wrapping
- Added `toggle-preview-wrap-word` action
```sh
fzf --preview 'bat --style=plain --color=always {}' \
--preview-window wrap-word \
--bind space:toggle-preview-wrap-word
```
- Added support for underline style variants in `--color`: `underline-double`, `underline-curly`, `underline-dotted`, `underline-dashed`
```sh
fzf --color 'fg:underline-curly,current-fg:underline-dashed'
```
- Added support for underline styles (`4:N`) and underline colors (SGR 58/59)
```sh
# In the list section
printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n' | fzf --ansi
# In the preview window
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'"
```
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview window
- Added `alt-gutter` color option (#4602) (@hedgieinsocks)
- Added `$FZF_WRAP` environment variable to child processes (`char` or `word` when wrapping is enabled) (#4672) (@bitraid)
- fish: Improved command history (CTRL-R) (#4672) (@bitraid)
- Enabled syntax highlighting in the list on fish 4.3.3+
- Added syntax-highlighted preview window that auto-shows for long or multi-line commands
- Added `ALT-ENTER` to reformat and insert selected commands
- Improved handling of bulk deletion of selected history entries (`SHIFT-DELETE`)
- Added fish completion support (#4605) (@lalvarezt)
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)
- Bug fixes
- Fixed `_fzf_compgen_{path,dir}` to respect `FZF_COMPLETION_{PATH,DIR}_OPTS` (#4592) (@shtse8, @LangLangBart)
- Fixed `--preview-window follow` not working correctly with wrapping (#3243, #4258)
- Fixed symlinks to directories being returned as files (#4676) (@skk64)
- Fixed SIGHUP signal handling (#4668) (@LangLangBart)
- Fixed preview process not killed on exit (#4667)
- Fixed coloring of items with zero-width characters (#4620)
- Fixed `track-current` unset after a combined movement action (#4649)
- Fixed `--accept-nth` being ignored in filter mode (#4636) (@charemma)
- Fixed display width calculation with `maxWidth` (#4596) (@LangLangBart)
- Fixed clearing of the rest of the current line on start (#4652)
- Fixed `x-api-key` header not required for GET requests (#4627)
- Fixed key reading not cancelled when `execute` triggered via a server request (#4653)
- Fixed rebind of readline command `redraw-current-line` (#4635) (@jameslazo)
- Fixed `fzf-tmux` `TERM` quoting and added `mktemp` usage (#4664) (@Goofygiraffe06)
- Do not allow very long queries in `FuzzyMatchV2` (#4608)
0.67.0
------
- Added `--freeze-left=N` option to keep the leftmost N columns always visible.
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+4 -5
View File
@@ -1,5 +1,4 @@
GO ?= go
DOCKER ?= docker
GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
@@ -193,12 +192,12 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
docker:
$(DOCKER) build -t fzf-ubuntu .
$(DOCKER) run -it fzf-ubuntu tmux
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu tmux
docker-test:
$(DOCKER) build -t fzf-ubuntu .
$(DOCKER) run -it fzf-ubuntu
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu
update:
$(GO) get -u
+1 -1
View File
@@ -493,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
+41 -99
View File
File diff suppressed because one or more lines are too long
+8 -7
View File
@@ -159,11 +159,11 @@ fi
set -e
# Clean up named pipes on exit
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/fzf-tmux-XXXXXX")
argsf="$tmpdir/args"
fifo1="$tmpdir/fifo1"
fifo2="$tmpdir/fifo2"
fifo3="$tmpdir/fifo3"
id=$RANDOM
argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
@@ -172,7 +172,8 @@ else
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
fi
cleanup() {
\rm -rf "$tmpdir"
\rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
eval "tmux ${tmux_win_opts[*]}"
@@ -195,7 +196,7 @@ cleanup() {
trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT
envs="export TERM=$(printf %q "$TERM") "
envs="export TERM=$TERM "
if [[ $opt =~ "-E" ]]; then
if [[ $tmux_version == 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
+1 -1
View File
@@ -503,7 +503,7 @@ LICENSE *fzf-license*
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
+20 -66
View File
@@ -2,7 +2,7 @@
set -u
version=0.68.0
version=0.67.0
auto_completion=
key_bindings=
update_config=2
@@ -243,16 +243,16 @@ fi
echo
for shell in $shells; do
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
[[ $shell == fish ]] && continue
src=${prefix_expand}.${shell}
echo -n "Generate $src ... "
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion"
fi
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
if [ $key_bindings -eq 0 ]; then
fzf_key_bindings="# $fzf_key_bindings"
fi
@@ -302,16 +302,15 @@ append_line() {
line="$2"
file="$3"
pat="${4:-}"
at_lno="${5:-}"
lines=""
echo "Update $file:"
echo " - $line"
if [ -f "$file" ]; then
if [[ -n $pat ]]; then
lines=$(\grep -nF "$pat" "$file")
if [ $# -lt 4 ]; then
lines=$(\grep -nF "$line" "$file")
else
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
lines=$(\grep -nF "$pat" "$file")
fi
fi
@@ -329,12 +328,8 @@ append_line() {
set -e
if [ "$update" -eq 1 ]; then
if [[ -z $at_lno ]]; then
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
else
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
fi
[ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file"
echo " + Added"
else
echo " ~ Skipped"
@@ -367,66 +362,25 @@ for shell in $shells; do
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
done
if [[ $shells =~ fish ]]; then
if [ $key_bindings -eq 1 ] && [[ $shells =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
mkdir -p "${fish_dir}/functions"
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf --fish | source' \
'end'
elif [[ $key_bindings -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
" $fzf_key_bindings" \
'end'
elif [[ $auto_completion -eq 1 ]]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
" $fzf_completion" \
'end'
fi
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
else
lno_func=0
fi
mkdir -p "${fish_dir}/functions"
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf --fish | source' \
'end'
else
echo "Check $bind_file:"
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -z $lno_func ]]; then
echo -e "function fish_user_key_bindings\nend" >> "$bind_file"
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
fi
lno_keys=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $lno_keys ]]; then
echo " ** Found 'fzf_key_bindings' in line #$lno_keys"
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
echo " ** You have to replace the line to 'fzf --fish | source'"
elif [[ $key_bindings -eq 1 ]]; then
echo " ** You have to replace the line to '$fzf_key_bindings'"
else
echo " ** You have to remove the line"
fi
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
if [[ -n $lno ]]; then
echo " ** Found 'fzf_key_bindings' in line #$lno"
echo " ** You have to replace the line to 'fzf --fish | source'"
echo
else
echo " - Clear"
echo
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " fzf --fish | source" "$bind_file" "" "$lno_func"
else
sed -i.~fzf_bak '/fzf --fish \| source/d' "$bind_file" && rm "$bind_file.~fzf_bak"
if [[ $key_bindings -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " $fzf_key_bindings" "$bind_file" "" "$lno_func"
elif [[ $auto_completion -eq 1 ]]; then
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
append_line $update_config " $fzf_completion" "$bind_file" "" "$lno_func"
fi
fi
append_line $update_config "fzf --fish | source" "$bind_file"
fi
fi
fi
@@ -439,7 +393,7 @@ if [ $update_config -eq 1 ]; then
echo
fi
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
[[ $shells =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
echo
echo 'Use uninstall script to remove fzf.'
echo
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.68.0"
$version="0.67.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+2 -5
View File
@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version = "0.68"
var version = "0.67"
var revision = "devel"
//go:embed shell/key-bindings.bash
@@ -29,9 +29,6 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish
var fishKeyBindings []byte
//go:embed shell/completion.fish
var fishCompletion []byte
//go:embed man/man1/fzf.1
var manPage []byte
@@ -68,7 +65,7 @@ func main() {
}
if options.Fish {
printScript("key-bindings.fish", fishKeyBindings)
printScript("completion.fish", fishCompletion)
fmt.Println("fzf_key_bindings")
return
}
if options.Help {
+2 -2
View File
@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -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 "Feb 2026" "fzf 0.68.0" "fzf\-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "Nov 2025" "fzf 0.67.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf\-tmux - open fzf in tmux split pane
+11 -34
View File
@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -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 "Feb 2026" "fzf 0.68.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Nov 2025" "fzf 0.67.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -272,7 +272,6 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
\fBgutter \fRGutter on the left
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
\fBalt\-bg \fRAlternate background color to create striped lines
\fBalt\-gutter \fRAlternate gutter color to create the striped pattern
\fBquery (input\-fg) \fRQuery string
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
@@ -326,14 +325,10 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
\fB#rrggbb \fR24-bit colors
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
\fBregular \fRClear previously set attributes; should precede the other ones
\fBstrip \fRRemove colors
\fBregular \fRClear previously set attributes; should precede the other ones
\fBstrip \fRRemove colors
\fBbold\fR
\fBunderline\fR
\fBunderline-double\fR
\fBunderline-curly\fR
\fBunderline-dotted\fR
\fBunderline-dashed\fR
\fBreverse\fR
\fBdim\fR
\fBitalic\fR
@@ -593,11 +588,8 @@ Highlight the whole current line
.B "\-\-cycle"
Enable cyclic scroll
.TP
.BI "\-\-wrap" "[=MODE]"
Enable line wrap. \fIMODE\fR can be \fBchar\fR (default) or \fBword\fR.
\fBword\fR mode wraps lines at word boundaries (spaces and tabs) instead of
at arbitrary character positions. \fB\-\-wrap\-word\fR is a synonym for
\fB\-\-wrap=word\fR.
.B "\-\-wrap"
Enable line wrap
.TP
.BI "\-\-wrap\-sign" "=INDICATOR"
Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
@@ -748,7 +740,7 @@ ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES.
e.g.
\fB# Prepend the current cursor position in yellow
fzf \-\-info\-command='printf "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
fzf \-\-info\-command='echo \-e "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
.TP
.B "\-\-no\-info"
@@ -848,9 +840,6 @@ e.g.
# This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
\fB# Use {+f} to get the selected items as a line-separated list
seq 100 | fzf \-\-multi \-\-bind 'enter:become:cat {+f}'\fR
Also,
* \fB{q}\fR is replaced to the current query string
@@ -919,11 +908,6 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.B * border\-bottom
.br
.TP
.BI "\-\-preview\-wrap\-sign" =INDICATOR
Indicator for wrapped lines in the preview window. If not set, the value of
\fB\-\-wrap\-sign\fR is used.
.TP
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
Position of the border label on the border line of the preview window. Specify
@@ -934,7 +918,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
border line.
.TP
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,wrap\-word][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.RS
.B POSITION: (default: right)
@@ -952,8 +936,7 @@ default until \fBtoggle\-preview\fR action is triggered.
execute the command in the background.
* Long lines are truncated by default. Line wrap can be enabled with
\fBwrap\fR flag. \fBwrap\-word\fR flag enables word-level wrapping, which
breaks lines at word boundaries instead of mid-word.
\fBwrap\fR flag.
* Preview window will automatically scroll to the bottom when \fBfollow\fR
flag is set, similarly to how \fBtail \-f\fR works.
@@ -1390,8 +1373,6 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_WRAP " The line wrapping mode (char, word) when enabled"
.br
.BR FZF_QUERY " Current query string"
.br
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
@@ -1881,7 +1862,6 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
\fBchange\-ghost(...)\fR (change ghost text to the given string)
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
\fBchange\-header\-lines(N)\fR (change the number of \fB\-\-header\-lines\fR)
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
@@ -1973,14 +1953,12 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle\-multi\-line\fR
\fBtoggle\-preview\fR
\fBtoggle\-preview\-wrap\fR
\fBtoggle\-preview\-wrap\-word\fR
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
\fBtoggle\-search\fR (toggle search functionality)
\fBtoggle\-sort\fR
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
\fBtoggle\-track\-current\fR (toggle tracking of the current item)
\fBtoggle\-wrap\fR
\fBtoggle\-wrap\-word\fR \fIctrl\-/\fR \fIalt\-/\fR
\fBtoggle\-wrap\fR \fIctrl\-/\fR \fIalt\-/\fR
\fBtoggle+down\fR \fIctrl\-i (tab)\fR
\fBtoggle+up\fR \fIbtab (shift\-tab)\fR
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
@@ -1988,7 +1966,6 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
\fBtransform\-header(...)\fR (transform header using an external command)
\fBtransform\-header\-lines(...)\fR (transform the number of \fB\-\-header\-lines\fR using an external command)
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
@@ -2128,7 +2105,7 @@ payload of HTTP POST request to the \fB\-\-listen\fR server.
e.g.
\fB# Disallow selecting an empty line
printf "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
echo \-e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\
\-\-bind 'enter:transform:[[ \-n {} ]] &&
echo accept ||
+1 -1
View File
@@ -1,4 +1,4 @@
" Copyright (c) 2013-2026 Junegunn Choi
" Copyright (c) 2013-2025 Junegunn Choi
"
" MIT License
"
-141
View File
@@ -1,141 +0,0 @@
function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
# Get tokens - use version-appropriate flags
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
# Filter out leading environment variable assignments
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
# Skip command prefixes so callers see the actual command name,
# e.g. "builtin cd""cd", "env VAR=1 command cd""cd"
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
# Set variables containing the major and minor fish version numbers, using
# a method compatible with all supported fish versions.
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.4.1
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
# fish v3.1b1 - v3.1.2
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
+13 -61
View File
@@ -124,7 +124,6 @@ _fzf_opts_completion() {
+i --no-ignore-case
+s --no-sort
+x --no-extended
--accept-nth
--ansi
--bash
--bind
@@ -138,88 +137,56 @@ _fzf_opts_completion() {
--expect
--filepath-word
--fish
--footer
--footer-border
--footer-label
--footer-label-pos
--freeze-left
--freeze-right
--gap
--gap-line
--ghost
--gutter
--gutter-raw
--header
--header-border
--header-first
--header-label
--header-label-pos
--header-lines
--header-lines-border
--height
--highlight-line
--history
--history-size
--hscroll-off
--info
--info-command
--input-border
--input-label
--input-label-pos
--jump-labels
--keep-right
--layout
--listen
--listen-unsafe
--list-border
--list-label
--list-label-pos
--literal
--man
--margin
--marker
--marker-multi-line
--min-height
--no-bold
--no-clear
--no-hscroll
--no-input
--no-multi-line
--no-mouse
--no-scrollbar
--no-separator
--no-unicode
--padding
--pointer
--preview
--preview-border
--preview-label
--preview-label-pos
--preview-window
--print-query
--print0
--prompt
--raw
--read0
--reverse
--scheme
--scroll-off
--scrollbar
--separator
--smart-case
--style
--sync
--tabstop
--tac
--tail
--tiebreak
--tmux
--track
--version
--walker
--walker-root
--walker-skip
--with-nth
--with-shell
--wrap
--wrap-sign
--preview-wrap-sign
--zsh
-0 --exit-0
-1 --select-1
@@ -239,11 +206,11 @@ _fzf_opts_completion() {
return 0
;;
--tiebreak)
COMPREPLY=($(compgen -W "length chunk pathname begin end index" -- "$cur"))
COMPREPLY=($(compgen -W "length chunk begin end index" -- "$cur"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "dark light base16 16 bw no" -- "$cur"))
COMPREPLY=($(compgen -W "dark light 16 bw no" -- "$cur"))
return 0
;;
--layout)
@@ -254,21 +221,12 @@ _fzf_opts_completion() {
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
return 0
;;
--wrap)
COMPREPLY=($(compgen -W "char word" -- "$cur"))
return 0
;;
--style)
COMPREPLY=($(compgen -W "default minimal full" -- "$cur"))
return 0
;;
--preview-window)
COMPREPLY=($(compgen -W "
default
hidden
nohidden
wrap
wrap-word
nowrap
cycle
nocycle
@@ -277,7 +235,6 @@ _fzf_opts_completion() {
left
right
rounded border border-rounded
border-line
sharp border-sharp
border-bold
border-block
@@ -291,16 +248,14 @@ _fzf_opts_completion() {
border-left
border-right
follow
nofollow
info
noinfo" -- "$cur"))
nofollow" -- "$cur"))
return 0
;;
--border | --list-border | --header-border | --header-lines-border | --footer-border | --input-border | --preview-border)
COMPREPLY=($(compgen -W "line rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
--border)
COMPREPLY=($(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
return 0
;;
--border-label-pos | --preview-label-pos | --list-label-pos | --header-label-pos | --footer-label-pos | --input-label-pos)
--border-label-pos | --preview-label-pos)
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
return 0
;;
@@ -370,18 +325,15 @@ __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 [[ $1 =~ dir ]]; then
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
else
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
fi
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
if [[ $1 =~ dir ]]; then
walker=dir,follow
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
else
walker=file,dir,follow,hidden
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
fi | while read -r item; do
-241
View File
@@ -1,241 +0,0 @@
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ completion.fish
#
# - $FZF_COMPLETION_OPTS (default: empty)
function fzf_completion_setup
#----BEGIN INCLUDE common.fish
# NOTE: Do not directly edit this section, which is copied from "common.fish".
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
# the changes. See code comments in "common.fish" for the implementation details.
function __fzf_defaults
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
$FZF_DEFAULT_OPTS $argv[2..-1]
end
function __fzfcmd
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
if test "$fish_major" -ge 4
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
end
if test -n "$fzf_query"
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
set -- dir $fzf_query
while not test -d "$dir"
set -- dir (dirname -z -- "$dir" | string split0)
end
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
if test "$fish_major" -ge 4
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
end
end
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Use complete builtin for specific commands
function __fzf_complete_native
set -l -- token (commandline -t)
set -l -- completions (eval complete -C \"$argv[1]\")
test -n "$completions"; or begin commandline -f repaint; return; end
# Calculate tabstop based on longest completion item (sample first 500 for performance)
set -l -- tabstop 20
set -l -- sample_size (math "min(500, "(count $completions)")")
for c in $completions[1..$sample_size]
set -l -- len (string length -V -- (string split -- \t $c))
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
and set -- tabstop $len[1]
end
# limit to 120 to prevent long lines
set -- tabstop (math "min($tabstop + 4, 120)")
set -l result
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
and begin
set -l -- tail ' '
# Append / to bare ~username results (fish omits it unlike other shells)
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
# Don't add trailing space if single result is a directory
test (count $result) -eq 1
and string match -q -- '*/' "$result"; and set -- tail ''
set -l -- result (string escape -n -- $result)
string match -q -- '~*' "$token"
and set result (string replace -r -- '^\\\\~' '~' $result)
string match -q -- '$*' "$token"
and set result (string replace -r -- '^\\\\\$' '\$' $result)
commandline -rt -- (string join ' ' -- $result)$tail
end
commandline -f repaint
end
function _fzf_complete
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
set -l -- fzf_query (commandline -t | string escape)
set -l result
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
and if functions -q $post_func
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
else
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
end
commandline -f repaint
end
# Kill completion (process selection)
function _fzf_complete_kill
set -l -- fzf_query (commandline -t | string escape)
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
set -lx FZF_DEFAULT_OPTS_FILE
if type -q ps
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
'or command ps --everyone --full --windows 2>/dev/null; end'
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
and commandline -rt -- (string join ' ' -- $result)" "
else
__fzf_complete_native "kill " --multi --query=$fzf_query
end
commandline -f repaint
end
# Main completion function
function fzf-completion
set -l -- tokens (__fzf_cmd_tokens)
set -l -- current_token (commandline -t)
set -l -- cmd_name $tokens[1]
# Route to appropriate completion function
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
_fzf_complete_$cmd_name $tokens
else
set -l -- fzf_opt --query=$current_token --multi
__fzf_complete_native "$tokens $current_token" $fzf_opt
end
end
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
if test (string match -r -- '^\d+' $version) -ge 4
bind shift-tab fzf-completion
bind -M insert shift-tab fzf-completion
else
bind -k btab fzf-completion
bind -M insert -k btab fzf-completion
end
end
# Run setup
fzf_completion_setup
+3 -6
View File
@@ -174,18 +174,15 @@ __fzf_generic_path_completion() {
export FZF_DEFAULT_OPTS
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if [[ $compgen =~ dir ]]; then
rest=${FZF_COMPLETION_DIR_OPTS-}
else
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" ${(Q)${(Z+n+)rest}}
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
if [[ $compgen =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__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
+4 -4
View File
@@ -121,7 +121,7 @@ else # awk - fallback for POSIX systems
fi
# Required to refresh the prompt after fzf
bind -m emacs-standard '"\C-\e(": redraw-current-line'
bind -m emacs-standard '"\er": redraw-current-line'
bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode'
@@ -130,7 +130,7 @@ bind -m emacs-standard '"\C-z": vi-editing-mode'
if ((BASH_VERSINFO[0] < 4)); then
# CTRL-T - Paste the selected file path into the command line
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\C-\e(\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
fi
@@ -140,7 +140,7 @@ if ((BASH_VERSINFO[0] < 4)); then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\C-\e("'
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
fi
@@ -165,7 +165,7 @@ fi
# ALT-C - cd into the selected directory
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\C-\e(\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi
+38 -88
View File
@@ -21,8 +21,8 @@
function fzf_key_bindings
# Check fish version
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
echo "This script requires fish version 3.1b1 or newer." >&2
return 1
else if not type -q fzf
@@ -30,12 +30,9 @@ function fzf_key_bindings
return 1
end
#----BEGIN INCLUDE common.fish
# NOTE: Do not directly edit this section, which is copied from "common.fish".
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
# the changes. See code comments in "common.fish" for the implementation details.
function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
string join ' ' -- \
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
@@ -54,51 +51,17 @@ function fzf_key_bindings
end
end
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
set -l tokens
if test (string match -r -- '^\d+' $version) -ge 4
set -- tokens (commandline -xpc)
else
set -- tokens (commandline -opc)
end
set -l -- var_count 0
for i in $tokens
if string match -qr -- '^[\w]+=' $i
set var_count (math $var_count + 1)
else
break
end
end
set -e -- tokens[0..$var_count]
while true
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
case '*'
break
end
end
string escape -n -- $tokens
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l fzf_query ''
set -l prefix ''
set -l dir '.'
# Set variables containing the major and minor fish version numbers, using
# a method compatible with all supported fish versions.
set -l -- fish_major (string match -r -- '^\d+' $version)
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
@@ -106,12 +69,16 @@ function fzf_key_bindings
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
end
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
set -- prefix (string match -r -- $prefix_regex $cl_token)
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
@@ -119,17 +86,22 @@ function fzf_key_bindings
end
if test -n "$fzf_query"
# Normalize path in $fzf_query, set $dir to the longest existing directory.
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
# fish v3.5.0 and newer
set -- fzf_query (path normalize -- $fzf_query)
set -- dir $fzf_query
while not path is -d $dir
set -- dir (path dirname $dir)
end
else
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.4.1
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
else
# fish v3.1b1 - v3.1.2
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
end
@@ -140,12 +112,16 @@ function fzf_key_bindings
end
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
# Strip $dir from $fzf_query - preserve trailing newlines.
if test "$fish_major" -ge 4
# fish v4.0.0 and newer
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
# fish v3.2.0 - v3.7.1 (last v3)
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
(string replace -- "$dir" '' $fzf_query | string collect -N)
else
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
end
@@ -154,7 +130,6 @@ function fzf_key_bindings
string escape -n -- "$dir" "$fzf_query" "$prefix"
end
#----END INCLUDE
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
@@ -165,13 +140,13 @@ function fzf_key_bindings
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
"--multi $FZF_CTRL_T_OPTS --print0")
"$FZF_CTRL_T_OPTS --multi --print0")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
commandline -f repaint
end
@@ -182,46 +157,24 @@ function fzf_key_bindings
set -l -- total_lines (count $command_line)
set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
# Add dynamic preview options if preview command isn't already set by user
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
# Convert the highlighted timestamp using the date command if available
set -l -- date_cmd '{1}'
if type -q date
if date -d @0 '+%s' 2>/dev/null | string match -q 0
# GNU date
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
# BSD date
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
end
end
# Prepend the options to allow user customizations
set -p -- FZF_DEFAULT_OPTS \
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
end
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
# Enable syntax highlighting colors on fish v4.3.3 and newer
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
set -a -- FZF_DEFAULT_OPTS '--ansi'
set -a -- FZF_DEFAULT_COMMAND '--color=always'
if type -q perl
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
else
set FZF_DEFAULT_COMMAND \
'set -l h (builtin history -z --reverse | string split0);' \
'for i in (seq (count $h) -1 1);' \
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end'
end
# Merge history from other sessions before searching
@@ -229,11 +182,11 @@ function fzf_key_bindings
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
if test "$total_lines" -eq 1
commandline -- $result
commandline -- (string replace -a -- \n\t \n $result)
else
set -l a (math $current_line - 1)
set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] $result
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
commandline -a -- '' $command_line[$b..-1]
end
end
@@ -281,6 +234,3 @@ function fzf_key_bindings
end
end
# Run setup
fzf_key_bindings
+6 -33
View File
@@ -128,52 +128,25 @@ fi
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
local selected extracted_with_perl=0
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
local selected
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
# 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
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,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
extracted_with_perl=1
else
selected="$(fc -rl 1 | __fzf_exec_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,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi
local ret=$?
local -a cmds
# Avoid leaking auto assigned values when using backreferences '(#b)'
local -a mbegin mend match
if [ -n "$selected" ]; then
# Heuristic to check if the selected value is from history or a custom query
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
# Split at newlines
for line in ${(ps:\n:)selected}; do
if (( extracted_with_perl )); then
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
fi
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
# Avoid $history array: lags behind 'fc' on foreign commands (*)
# https://zsh.org/mla/users/2024/msg00692.html
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
zle .push-line
zle vi-fetch-history -n ${match[1]}
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
BUFFER=""
zle .get-line
fi
done
if (( ${#cmds[@]} )); then
# Join by newline after stripping trailing newlines from each command
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
CURSOR=${#BUFFER}
fi
if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
zle vi-fetch-history -n $MATCH
else # selected is a custom query, not from history
LBUFFER="$selected"
fi
+12 -14
View File
@@ -8,26 +8,24 @@ dir=${0%"${0##*/}"}
update() {
{
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
cat << EOF
# NOTE: Do not directly edit this section, which is copied from "$1".
# To modify it, one can edit "$1" and run "./update.sh" to apply
# the changes. See code comments in "$1" for the implementation details.
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
EOF
echo
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
sed -n '/^#----END INCLUDE/,$p' "$2"
} > "$2.part"
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
sed -n '/^#----END INCLUDE/,$p' "$1"
} > "$1.part"
mv -f "$2.part" "$2"
mv -f "$1.part" "$1"
}
update "common.sh" "$dir/completion.bash"
update "common.sh" "$dir/completion.zsh"
update "common.sh" "$dir/key-bindings.bash"
update "common.sh" "$dir/key-bindings.zsh"
update "common.fish" "$dir/completion.fish"
update "common.fish" "$dir/key-bindings.fish"
update "$dir/completion.bash"
update "$dir/completion.zsh"
update "$dir/key-bindings.bash"
update "$dir/key-bindings.zsh"
# Check if --check is in ARGV
check=0
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013-2026 Junegunn Choi
Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+150 -155
View File
@@ -30,164 +30,159 @@ func _() {
_ = x[actChangeBorderLabel-19]
_ = x[actChangeGhost-20]
_ = x[actChangeHeader-21]
_ = x[actChangeHeaderLines-22]
_ = x[actChangeFooter-23]
_ = x[actChangeHeaderLabel-24]
_ = x[actChangeFooterLabel-25]
_ = x[actChangeInputLabel-26]
_ = x[actChangeListLabel-27]
_ = x[actChangeMulti-28]
_ = x[actChangeNth-29]
_ = x[actChangePointer-30]
_ = x[actChangePreview-31]
_ = x[actChangePreviewLabel-32]
_ = x[actChangePreviewWindow-33]
_ = x[actChangePrompt-34]
_ = x[actChangeQuery-35]
_ = x[actClearScreen-36]
_ = x[actClearQuery-37]
_ = x[actClearSelection-38]
_ = x[actClose-39]
_ = x[actDeleteChar-40]
_ = x[actDeleteCharEof-41]
_ = x[actEndOfLine-42]
_ = x[actFatal-43]
_ = x[actForwardChar-44]
_ = x[actForwardWord-45]
_ = x[actForwardSubWord-46]
_ = x[actKillLine-47]
_ = x[actKillWord-48]
_ = x[actKillSubWord-49]
_ = x[actUnixLineDiscard-50]
_ = x[actUnixWordRubout-51]
_ = x[actYank-52]
_ = x[actBackwardKillWord-53]
_ = x[actBackwardKillSubWord-54]
_ = x[actSelectAll-55]
_ = x[actDeselectAll-56]
_ = x[actToggle-57]
_ = x[actToggleSearch-58]
_ = x[actToggleAll-59]
_ = x[actToggleDown-60]
_ = x[actToggleUp-61]
_ = x[actToggleIn-62]
_ = x[actToggleOut-63]
_ = x[actToggleTrack-64]
_ = x[actToggleTrackCurrent-65]
_ = x[actToggleHeader-66]
_ = x[actToggleWrap-67]
_ = x[actToggleWrapWord-68]
_ = x[actToggleMultiLine-69]
_ = x[actToggleHscroll-70]
_ = x[actToggleRaw-71]
_ = x[actEnableRaw-72]
_ = x[actDisableRaw-73]
_ = x[actTrackCurrent-74]
_ = x[actToggleInput-75]
_ = x[actHideInput-76]
_ = x[actShowInput-77]
_ = x[actUntrackCurrent-78]
_ = x[actDown-79]
_ = x[actDownMatch-80]
_ = x[actUp-81]
_ = x[actUpMatch-82]
_ = x[actPageUp-83]
_ = x[actPageDown-84]
_ = x[actPosition-85]
_ = x[actHalfPageUp-86]
_ = x[actHalfPageDown-87]
_ = x[actOffsetUp-88]
_ = x[actOffsetDown-89]
_ = x[actOffsetMiddle-90]
_ = x[actJump-91]
_ = x[actJumpAccept-92]
_ = x[actPrintQuery-93]
_ = x[actRefreshPreview-94]
_ = x[actReplaceQuery-95]
_ = x[actToggleSort-96]
_ = x[actShowPreview-97]
_ = x[actHidePreview-98]
_ = x[actTogglePreview-99]
_ = x[actTogglePreviewWrap-100]
_ = x[actTogglePreviewWrapWord-101]
_ = x[actTransform-102]
_ = x[actTransformBorderLabel-103]
_ = x[actTransformGhost-104]
_ = x[actTransformHeader-105]
_ = x[actTransformHeaderLines-106]
_ = x[actTransformFooter-107]
_ = x[actTransformHeaderLabel-108]
_ = x[actTransformFooterLabel-109]
_ = x[actTransformInputLabel-110]
_ = x[actTransformListLabel-111]
_ = x[actTransformNth-112]
_ = x[actTransformPointer-113]
_ = x[actTransformPreviewLabel-114]
_ = x[actTransformPrompt-115]
_ = x[actTransformQuery-116]
_ = x[actTransformSearch-117]
_ = x[actTrigger-118]
_ = x[actBgTransform-119]
_ = x[actBgTransformBorderLabel-120]
_ = x[actBgTransformGhost-121]
_ = x[actBgTransformHeader-122]
_ = x[actBgTransformHeaderLines-123]
_ = x[actBgTransformFooter-124]
_ = x[actBgTransformHeaderLabel-125]
_ = x[actBgTransformFooterLabel-126]
_ = x[actBgTransformInputLabel-127]
_ = x[actBgTransformListLabel-128]
_ = x[actBgTransformNth-129]
_ = x[actBgTransformPointer-130]
_ = x[actBgTransformPreviewLabel-131]
_ = x[actBgTransformPrompt-132]
_ = x[actBgTransformQuery-133]
_ = x[actBgTransformSearch-134]
_ = x[actBgCancel-135]
_ = x[actSearch-136]
_ = x[actPreview-137]
_ = x[actPreviewTop-138]
_ = x[actPreviewBottom-139]
_ = x[actPreviewUp-140]
_ = x[actPreviewDown-141]
_ = x[actPreviewPageUp-142]
_ = x[actPreviewPageDown-143]
_ = x[actPreviewHalfPageUp-144]
_ = x[actPreviewHalfPageDown-145]
_ = x[actPrevHistory-146]
_ = x[actPrevSelected-147]
_ = x[actPrint-148]
_ = x[actPut-149]
_ = x[actNextHistory-150]
_ = x[actNextSelected-151]
_ = x[actExecute-152]
_ = x[actExecuteSilent-153]
_ = x[actExecuteMulti-154]
_ = x[actSigStop-155]
_ = x[actBest-156]
_ = x[actFirst-157]
_ = x[actLast-158]
_ = x[actReload-159]
_ = x[actReloadSync-160]
_ = x[actDisableSearch-161]
_ = x[actEnableSearch-162]
_ = x[actSelect-163]
_ = x[actDeselect-164]
_ = x[actUnbind-165]
_ = x[actRebind-166]
_ = x[actToggleBind-167]
_ = x[actBecome-168]
_ = x[actShowHeader-169]
_ = x[actHideHeader-170]
_ = x[actBell-171]
_ = x[actExclude-172]
_ = x[actExcludeMulti-173]
_ = x[actAsync-174]
_ = x[actChangeFooter-22]
_ = x[actChangeHeaderLabel-23]
_ = x[actChangeFooterLabel-24]
_ = x[actChangeInputLabel-25]
_ = x[actChangeListLabel-26]
_ = x[actChangeMulti-27]
_ = x[actChangeNth-28]
_ = x[actChangePointer-29]
_ = x[actChangePreview-30]
_ = x[actChangePreviewLabel-31]
_ = x[actChangePreviewWindow-32]
_ = x[actChangePrompt-33]
_ = x[actChangeQuery-34]
_ = x[actClearScreen-35]
_ = x[actClearQuery-36]
_ = x[actClearSelection-37]
_ = x[actClose-38]
_ = x[actDeleteChar-39]
_ = x[actDeleteCharEof-40]
_ = x[actEndOfLine-41]
_ = x[actFatal-42]
_ = x[actForwardChar-43]
_ = x[actForwardWord-44]
_ = x[actForwardSubWord-45]
_ = x[actKillLine-46]
_ = x[actKillWord-47]
_ = x[actKillSubWord-48]
_ = x[actUnixLineDiscard-49]
_ = x[actUnixWordRubout-50]
_ = x[actYank-51]
_ = x[actBackwardKillWord-52]
_ = x[actBackwardKillSubWord-53]
_ = x[actSelectAll-54]
_ = x[actDeselectAll-55]
_ = x[actToggle-56]
_ = x[actToggleSearch-57]
_ = x[actToggleAll-58]
_ = x[actToggleDown-59]
_ = x[actToggleUp-60]
_ = x[actToggleIn-61]
_ = x[actToggleOut-62]
_ = x[actToggleTrack-63]
_ = x[actToggleTrackCurrent-64]
_ = x[actToggleHeader-65]
_ = x[actToggleWrap-66]
_ = x[actToggleMultiLine-67]
_ = x[actToggleHscroll-68]
_ = x[actToggleRaw-69]
_ = x[actEnableRaw-70]
_ = x[actDisableRaw-71]
_ = x[actTrackCurrent-72]
_ = x[actToggleInput-73]
_ = x[actHideInput-74]
_ = x[actShowInput-75]
_ = x[actUntrackCurrent-76]
_ = x[actDown-77]
_ = x[actDownMatch-78]
_ = x[actUp-79]
_ = x[actUpMatch-80]
_ = x[actPageUp-81]
_ = x[actPageDown-82]
_ = x[actPosition-83]
_ = x[actHalfPageUp-84]
_ = x[actHalfPageDown-85]
_ = x[actOffsetUp-86]
_ = x[actOffsetDown-87]
_ = x[actOffsetMiddle-88]
_ = x[actJump-89]
_ = x[actJumpAccept-90]
_ = x[actPrintQuery-91]
_ = x[actRefreshPreview-92]
_ = x[actReplaceQuery-93]
_ = x[actToggleSort-94]
_ = x[actShowPreview-95]
_ = x[actHidePreview-96]
_ = x[actTogglePreview-97]
_ = x[actTogglePreviewWrap-98]
_ = x[actTransform-99]
_ = x[actTransformBorderLabel-100]
_ = x[actTransformGhost-101]
_ = x[actTransformHeader-102]
_ = x[actTransformFooter-103]
_ = x[actTransformHeaderLabel-104]
_ = x[actTransformFooterLabel-105]
_ = x[actTransformInputLabel-106]
_ = x[actTransformListLabel-107]
_ = x[actTransformNth-108]
_ = x[actTransformPointer-109]
_ = x[actTransformPreviewLabel-110]
_ = x[actTransformPrompt-111]
_ = x[actTransformQuery-112]
_ = x[actTransformSearch-113]
_ = x[actTrigger-114]
_ = x[actBgTransform-115]
_ = x[actBgTransformBorderLabel-116]
_ = x[actBgTransformGhost-117]
_ = x[actBgTransformHeader-118]
_ = x[actBgTransformFooter-119]
_ = x[actBgTransformHeaderLabel-120]
_ = x[actBgTransformFooterLabel-121]
_ = x[actBgTransformInputLabel-122]
_ = x[actBgTransformListLabel-123]
_ = x[actBgTransformNth-124]
_ = x[actBgTransformPointer-125]
_ = x[actBgTransformPreviewLabel-126]
_ = x[actBgTransformPrompt-127]
_ = x[actBgTransformQuery-128]
_ = x[actBgTransformSearch-129]
_ = x[actBgCancel-130]
_ = x[actSearch-131]
_ = x[actPreview-132]
_ = x[actPreviewTop-133]
_ = x[actPreviewBottom-134]
_ = x[actPreviewUp-135]
_ = x[actPreviewDown-136]
_ = x[actPreviewPageUp-137]
_ = x[actPreviewPageDown-138]
_ = x[actPreviewHalfPageUp-139]
_ = x[actPreviewHalfPageDown-140]
_ = x[actPrevHistory-141]
_ = x[actPrevSelected-142]
_ = x[actPrint-143]
_ = x[actPut-144]
_ = x[actNextHistory-145]
_ = x[actNextSelected-146]
_ = x[actExecute-147]
_ = x[actExecuteSilent-148]
_ = x[actExecuteMulti-149]
_ = x[actSigStop-150]
_ = x[actBest-151]
_ = x[actFirst-152]
_ = x[actLast-153]
_ = x[actReload-154]
_ = x[actReloadSync-155]
_ = x[actDisableSearch-156]
_ = x[actEnableSearch-157]
_ = x[actSelect-158]
_ = x[actDeselect-159]
_ = x[actUnbind-160]
_ = x[actRebind-161]
_ = x[actToggleBind-162]
_ = x[actBecome-163]
_ = x[actShowHeader-164]
_ = x[actHideHeader-165]
_ = x[actBell-166]
_ = x[actExclude-167]
_ = x[actExcludeMulti-168]
_ = x[actAsync-169]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600}
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1402, 1425, 1442, 1460, 1478, 1501, 1524, 1546, 1567, 1582, 1601, 1625, 1643, 1660, 1678, 1688, 1702, 1727, 1746, 1766, 1786, 1811, 1836, 1860, 1883, 1900, 1921, 1947, 1967, 1986, 2006, 2017, 2026, 2036, 2049, 2065, 2077, 2091, 2107, 2125, 2145, 2167, 2181, 2196, 2204, 2210, 2224, 2239, 2249, 2265, 2280, 2290, 2297, 2305, 2312, 2321, 2334, 2350, 2365, 2374, 2385, 2394, 2403, 2416, 2425, 2438, 2451, 2458, 2468, 2483, 2491}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {
+7 -9
View File
@@ -445,9 +445,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
// Also, we should not allow a very long pattern to avoid 16-bit integer
// overflow in the score matrix. 1000 is a safe limit.
if slab != nil && N*M > cap(slab.I16) || M > 1000 {
if slab != nil && N*M > cap(slab.I16) {
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
}
@@ -503,7 +501,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if pidx < M {
F[pidx] = int32(off)
pidx++
pchar = pattern[min(pidx, M-1)]
pchar = pattern[util.Min(pidx, M-1)]
}
lastIdx = off
}
@@ -521,9 +519,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false
} else {
if inGap {
H0[off] = max(prevH0+scoreGapExtension, 0)
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else {
H0[off] = max(prevH0+scoreGapStart, 0)
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
}
C0[off] = 0
inGap = true
@@ -589,7 +587,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
if b >= bonusBoundary && b > fb {
consecutive = 1
} else {
b = max(b, bonusConsecutive, fb)
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
}
}
if s1+b < s2 {
@@ -602,7 +600,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
Csub[off] = consecutive
inGap = s1 < s2
score := max(s1, s2, 0)
score := util.Max16(util.Max16(s1, s2), 0)
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, col
}
@@ -686,7 +684,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
if bonus >= bonusBoundary && bonus > firstBonus {
firstBonus = bonus
}
bonus = max(bonus, firstBonus, bonusConsecutive)
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
}
if pidx == 0 {
score += int(bonus * bonusFirstCharMultiplier)
+1 -1
View File
@@ -86,7 +86,7 @@ func TestFuzzyMatch(t *testing.T) {
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
max(bonusCamel123, int(bonusBoundaryWhite)))
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
// Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
+17 -81
View File
@@ -22,21 +22,20 @@ type url struct {
type ansiState struct {
fg tui.Color
bg tui.Color
ul tui.Color
attr tui.Attr
lbg tui.Color
url *url
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.ul != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
return s.fg == t.fg && s.bg == t.bg && s.ul == t.ul && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
}
func (s *ansiState) ToString() string {
@@ -55,18 +54,7 @@ func (s *ansiState) ToString() string {
ret += "3;"
}
if s.attr&tui.Underline > 0 {
switch s.attr.UnderlineStyle() {
case tui.UlStyleDouble:
ret += "4:2;"
case tui.UlStyleCurly:
ret += "4:3;"
case tui.UlStyleDotted:
ret += "4:4;"
case tui.UlStyleDashed:
ret += "4:5;"
default:
ret += "4;"
}
ret += "4;"
}
if s.attr&tui.Blink > 0 {
ret += "5;"
@@ -78,9 +66,6 @@ func (s *ansiState) ToString() string {
ret += "9;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
if s.ul != -1 {
ret += toAnsiStringUl(s.ul)
}
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
if s.url != nil {
@@ -89,20 +74,6 @@ func (s *ansiState) ToString() string {
return ret
}
func toAnsiStringUl(color tui.Color) string {
col := int(color)
if col < 0 {
return ""
}
if col >= (1 << 24) {
r := strconv.Itoa((col >> 16) & 0xff)
g := strconv.Itoa((col >> 8) & 0xff)
b := strconv.Itoa(col & 0xff)
return "58;2;" + r + ";" + g + ";" + b + ";"
}
return "58;5;" + strconv.Itoa(col) + ";"
}
func toAnsiString(color tui.Color, offset int) string {
col := int(color)
ret := ""
@@ -367,19 +338,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
return trimmed, nil, state
}
func parseAnsiCode(s string) (int, byte, string) {
func parseAnsiCode(s string) (int, string) {
var remaining string
var sep byte
// Find the first separator (either ; or :)
i := -1
for j := 0; j < len(s); j++ {
if s[j] == ';' || s[j] == ':' {
i = j
break
}
var i int
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
}
if i >= 0 {
sep = s[i]
remaining = s[i+1:]
s = s[:i]
}
@@ -391,14 +358,14 @@ func parseAnsiCode(s string) (int, byte, string) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, sep, remaining
return -1, remaining
}
code = code*10 + int(ch)
}
return code, sep, remaining
return code, remaining
}
return -1, sep, remaining
return -1, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
@@ -406,14 +373,14 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
if prevState != nil {
return *prevState
}
return ansiState{-1, -1, -1, 0, -1, nil}
return ansiState{-1, -1, 0, -1, nil}
}
var state ansiState
if prevState == nil {
state = ansiState{-1, -1, -1, 0, -1, nil}
state = ansiState{-1, -1, 0, -1, nil}
} else {
state = ansiState{prevState.fg, prevState.bg, prevState.ul, prevState.attr, prevState.lbg, prevState.url}
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
}
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
@@ -438,7 +405,6 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
reset := func() {
state.fg = -1
state.bg = -1
state.ul = -1
state.attr = 0
}
@@ -454,8 +420,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
count := 0
for len(ansiCode) != 0 {
var num int
var sep byte
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
count++
switch state256 {
case 0:
@@ -466,15 +431,10 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 48:
ptr = &state.bg
state256++
case 58:
ptr = &state.ul
state256++
case 39:
state.fg = -1
case 49:
state.bg = -1
case 59:
state.ul = -1
case 1:
state.attr = state.attr | tui.Bold
case 2:
@@ -482,30 +442,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 3:
state.attr = state.attr | tui.Italic
case 4:
if sep == ':' {
// SGR 4:N — underline style sub-parameter
var subNum int
subNum, _, ansiCode = parseAnsiCode(ansiCode)
state.attr = state.attr &^ tui.UnderlineStyleMask
switch subNum {
case 0:
state.attr = state.attr &^ tui.Underline
case 1:
state.attr = state.attr | tui.Underline
case 2:
state.attr = state.attr | tui.Underline | tui.UlStyleDouble
case 3:
state.attr = state.attr | tui.Underline | tui.UlStyleCurly
case 4:
state.attr = state.attr | tui.Underline | tui.UlStyleDotted
case 5:
state.attr = state.attr | tui.Underline | tui.UlStyleDashed
default:
state.attr = state.attr | tui.Underline
}
} else {
state.attr = state.attr | tui.Underline
}
state.attr = state.attr | tui.Underline
case 5:
state.attr = state.attr | tui.Blink
case 7:
@@ -519,7 +456,6 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.attr = state.attr &^ tui.Italic
case 24: // tput rmul
state.attr = state.attr &^ tui.Underline
state.attr = state.attr &^ tui.UnderlineStyleMask
case 25:
state.attr = state.attr &^ tui.Blink
case 27:
+17 -123
View File
@@ -369,10 +369,10 @@ func TestAnsiCodeStringConversion(t *testing.T) {
}
}
assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, ul: -1, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m")
@@ -380,142 +380,36 @@ func TestAnsiCodeStringConversion(t *testing.T) {
assert("\x1b[92m", nil, "\x1b[92;49m")
assert("\x1b[102m", nil, "\x1b[39;102m")
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, ul: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;7m",
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1, ul: -1},
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
// Underline styles
assert("\x1b[4:3m", nil, "\x1b[4:3;39;49m")
assert("\x1b[4:2m", nil, "\x1b[4:2;39;49m")
assert("\x1b[4:4m", nil, "\x1b[4:4;39;49m")
assert("\x1b[4:5m", nil, "\x1b[4:5;39;49m")
assert("\x1b[4:1m", nil, "\x1b[4;39;49m")
// Underline color (256-color)
assert("\x1b[4;58;5;100m", nil, "\x1b[4;39;49;58;5;100m")
// Underline color (24-bit)
assert("\x1b[4;58;2;255;0;128m", nil, "\x1b[4;39;49;58;2;255;0;128m")
// Curly underline + underline color
assert("\x1b[4:3;58;2;255;0;0m", nil, "\x1b[4:3;39;49;58;2;255;0;0m")
// SGR 59 resets underline color
assert("\x1b[59m", &ansiState{fg: 1, bg: -1, ul: 100, lbg: -1}, "\x1b[31;49m")
}
func TestParseAnsiCode(t *testing.T) {
tests := []struct {
In string
Exp string
N int
Sep byte
In, Exp string
N int
}{
{"123", "", 123, 0},
{"1a", "", -1, 0},
{"1a;12", "12", -1, ';'},
{"12;a", "a", 12, ';'},
{"-2", "", -1, 0},
// Colon sub-parameters: earliest separator wins (@shtse8)
{"4:3", "3", 4, ':'},
{"4:3;31", "3;31", 4, ':'},
{"38:2:255:0:0", "2:255:0:0", 38, ':'},
{"58:5:200", "5:200", 58, ':'},
// Semicolon before colon
{"4;38:2:0:0:0", "38:2:0:0:0", 4, ';'},
{"123", "", 123},
{"1a", "", -1},
{"1a;12", "12", -1},
{"12;a", "a", 12},
{"-2", "", -1},
}
for _, x := range tests {
n, sep, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp || sep != x.Sep {
t.Fatalf("%q: got: (%d %q %q) want: (%d %q %q)", x.In, n, s, string(sep), x.N, x.Exp, string(x.Sep))
n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
}
}
}
// Test cases adapted from @shtse8 (PR #4678)
func TestInterpretCodeUnderlineStyles(t *testing.T) {
// 4:0 = no underline
state := interpretCode("\x1b[4:0m", nil)
if state.attr&tui.Underline != 0 {
t.Error("4:0 should not set underline")
}
// 4:1 = single underline
state = interpretCode("\x1b[4:1m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:1 should set underline")
}
// 4:3 = curly underline
state = interpretCode("\x1b[4:3m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:3 should set underline")
}
if state.attr.UnderlineStyle() != tui.UlStyleCurly {
t.Error("4:3 should set curly underline style")
}
// 4:3 should NOT set italic (3 is a sub-param, not SGR 3)
if state.attr&tui.Italic != 0 {
t.Error("4:3 should not set italic")
}
// 4:2;31 = double underline + red fg
state = interpretCode("\x1b[4:2;31m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4:2;31 should set underline")
}
if state.fg != 1 {
t.Errorf("4:2;31 should set fg to red (1), got %d", state.fg)
}
if state.attr&tui.Dim != 0 {
t.Error("4:2;31 should not set dim")
}
// Plain 4 still works
state = interpretCode("\x1b[4m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4 should set underline")
}
// 4;2 (semicolon) = underline + dim
state = interpretCode("\x1b[4;2m", nil)
if state.attr&tui.Underline == 0 {
t.Error("4;2 should set underline")
}
if state.attr&tui.Dim == 0 {
t.Error("4;2 should set dim")
}
}
// Test cases adapted from @shtse8 (PR #4678)
func TestInterpretCodeUnderlineColor(t *testing.T) {
// 58:2:R:G:B should not affect fg or bg
state := interpretCode("\x1b[58:2:255:0:0m", nil)
if state.fg != -1 || state.bg != -1 {
t.Errorf("58:2:R:G:B should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
}
// 58:5:200 should not affect fg or bg
state = interpretCode("\x1b[58:5:200m", nil)
if state.fg != -1 || state.bg != -1 {
t.Errorf("58:5:N should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
}
// 58:2:R:G:B combined with 38:2:R:G:B should only set fg
state = interpretCode("\x1b[58:2:255:0:0;38:2:0:255:0m", nil)
expectedFg := tui.Color(1<<24 | 0<<16 | 255<<8 | 0)
if state.fg != expectedFg {
t.Errorf("expected fg=%d, got %d", expectedFg, state.fg)
}
if state.bg != -1 {
t.Errorf("bg should be -1, got %d", state.bg)
}
}
// kernel/bpf/preload/iterators/README
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
-14
View File
@@ -52,20 +52,6 @@ func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1]
}
// GetItems returns the first n items from the given chunks
func GetItems(chunks []*Chunk, n int) []Item {
items := make([]Item, 0, n)
for _, chunk := range chunks {
for i := 0; i < chunk.count && len(items) < n; i++ {
items = append(items, chunk.items[i])
}
if len(items) >= n {
break
}
}
return items
}
// CountItems returns the total number of Items
func CountItems(cs []*Chunk) int {
if len(cs) == 0 {
+2 -1
View File
@@ -39,7 +39,7 @@ const (
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk
chunkSize int = 1000
chunkSize int = 100
// Pre-allocated memory slices to minimize GC
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
@@ -65,6 +65,7 @@ const (
EvtSearchNew
EvtSearchProgress
EvtSearchFin
EvtHeader
EvtReady
EvtQuit
)
+38 -93
View File
@@ -2,7 +2,6 @@
package fzf
import (
"fmt"
"maps"
"os"
"sync"
@@ -18,6 +17,7 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/
type revision struct {
@@ -38,18 +38,6 @@ func (r revision) compatible(other revision) bool {
return r.major == other.major
}
func buildItemTransformer(opts *Options) func(*Item) string {
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
return func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
return func(item *Item) string {
return item.AsString(opts.Ansi)
}
}
// Run starts fzf
func Run(opts *Options) (int, error) {
if opts.Filter == nil {
@@ -113,8 +101,14 @@ func Run(opts *Options) (int, error) {
cache := NewChunkCache()
var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines)
if opts.WithNth == nil {
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(data)
item.text.Index = itemIndex
itemIndex++
@@ -141,6 +135,11 @@ func Run(opts *Options) (int, error) {
}
}
transformed := nthTransformer(tokens, itemIndex)
if len(header) < opts.HeaderLines {
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(stringBytes(transformed))
// We should not trim trailing whitespaces with background colors
@@ -148,7 +147,7 @@ func Run(opts *Options) (int, error) {
if item.colors != nil {
for _, ansi := range *item.colors {
if ansi.color.bg >= 0 {
maxColorOffset = max(maxColorOffset, ansi.offset[1])
maxColorOffset = util.Max32(maxColorOffset, ansi.offset[1])
}
}
}
@@ -182,7 +181,7 @@ func Run(opts *Options) (int, error) {
}
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
@@ -225,17 +224,15 @@ func Run(opts *Options) (int, error) {
denylist = make(map[int32]struct{})
denyMutex.Unlock()
}
headerLines := int32(opts.HeaderLines)
headerUpdated := false
patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock()
denylistCopy := maps.Clone(denylist)
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode
if opts.Filter != nil {
@@ -246,8 +243,6 @@ func Run(opts *Options) (int, error) {
pattern := patternBuilder([]rune(*opts.Filter))
matcher.sort = pattern.sortable
transformer := buildItemTransformer(opts)
found := false
if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size)
@@ -256,12 +251,9 @@ func Run(opts *Options) (int, error) {
func(runes []byte) bool {
item := Item{}
if chunkList.trans(&item, runes) {
if item.Index() < headerLines {
return false
}
mutex.Lock()
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
opts.Printer(transformer(&item))
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
found = true
}
mutex.Unlock()
@@ -275,51 +267,11 @@ func Run(opts *Options) (int, error) {
// NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
if opts.Bench > 0 {
// Benchmark mode: repeat scan for the given duration
totalItems := CountItems(snapshot)
var matchCount int
var times []time.Duration
deadline := time.Now().Add(opts.Bench)
for time.Now().Before(deadline) {
cache.Clear()
start := time.Now()
result := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
times = append(times, time.Since(start))
matchCount = result.merger.Length()
}
// Print stats
var total time.Duration
minD, maxD := times[0], times[0]
for _, d := range times {
total += d
if d < minD {
minD = d
}
if d > maxD {
maxD = d
}
}
avg := total / time.Duration(len(times))
selectivity := float64(matchCount) / float64(totalItems) * 100
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%)\n",
len(times),
float64(avg.Microseconds())/1000,
float64(minD.Microseconds())/1000,
float64(maxD.Microseconds())/1000,
total.Seconds(),
totalItems, matchCount, selectivity)
return ExitOk, nil
}
result := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
for i := 0; i < result.merger.Length(); i++ {
opts.Printer(transformer(result.merger.Get(i).item))
opts.Printer(result.merger.Get(i).item.AsString(opts.Ansi))
found = true
}
}
@@ -364,11 +316,10 @@ func Run(opts *Options) (int, error) {
query := []rune{}
determine := func(final bool) {
if heightUnknown {
items := max(0, total-int(headerLines))
if items >= maxFit || final {
if total >= maxFit || final {
deferred = false
heightUnknown = false
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
}
} else if deferred {
deferred = false
@@ -384,11 +335,11 @@ func Run(opts *Options) (int, error) {
clearDenylist()
}
reading = true
headerUpdated = false
startTick = ticks
chunkList.Clear()
itemIndex = 0
inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines)
readyChan := make(chan bool)
go reader.restart(command, environ, readyChan)
<-readyChan
@@ -446,11 +397,7 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
if headerLines > 0 && !headerUpdated {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
headerUpdated = int32(total) >= headerLines
}
terminal.UpdateCount(total, !reading, value.(*string))
if heightUnknown && !deferred {
determine(!reading)
}
@@ -460,7 +407,6 @@ func Run(opts *Options) (int, error) {
var command *commandSpec
var environ []string
var changed bool
headerLinesChanged := false
switch val := value.(type) {
case searchRequest:
sort = val.sort
@@ -481,12 +427,6 @@ func Run(opts *Options) (int, error) {
nth = *val.nth
bump = true
}
if val.headerLines != nil {
headerLines = int32(*val.headerLines)
headerUpdated = false
headerLinesChanged = true
bump = true
}
if bump {
patternCache = make(map[string]*Pattern)
cache.Clear()
@@ -523,14 +463,6 @@ func Run(opts *Options) (int, error) {
snapshotRevision = inputRevision
}
}
if headerLinesChanged {
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
if headerLines > 0 {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
} else {
terminal.UpdateHeader(nil)
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false
@@ -540,6 +472,11 @@ func Run(opts *Options) (int, error) {
terminal.UpdateProgress(val)
}
case EvtHeader:
headerPadded := make([]string, opts.HeaderLines)
copy(headerPadded, value.([]string))
terminal.UpdateHeader(headerPadded)
case EvtSearchFin:
switch val := value.(type) {
case MatchResult:
@@ -556,7 +493,15 @@ func Run(opts *Options) (int, error) {
if len(opts.Expect) > 0 {
opts.Printer("")
}
transformer := buildItemTransformer(opts)
transformer := func(item *Item) string {
return item.AsString(opts.Ansi)
}
if opts.AcceptNth != nil {
fn := opts.AcceptNth(opts.Delimiter)
transformer = func(item *Item) string {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
}
}
for i := range count {
opts.Printer(transformer(merger.Get(i).item))
}
@@ -579,7 +524,7 @@ func Run(opts *Options) (int, error) {
break
}
if delay && reading {
dur := util.Constrain(
dur := util.DurWithin(
time.Duration(ticks-startTick)*coordinatorDelayStep,
0, coordinatorDelayMax)
time.Sleep(dur)
+9 -9
View File
@@ -3,6 +3,7 @@ package fzf
import (
"fmt"
"runtime"
"sort"
"sync"
"time"
@@ -42,7 +43,6 @@ type Matcher struct {
reqBox *util.EventBox
partitions int
slab []*util.Slab
sortBuf [][]Result
mergerCache map[string]MatchResult
revision revision
}
@@ -54,11 +54,8 @@ const (
// NewMatcher returns a new Matcher
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
if threads > 0 {
partitions = threads
}
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
@@ -68,7 +65,6 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(),
partitions: partitions,
slab: make([]*util.Slab, partitions),
sortBuf: make([][]Result, partitions),
mergerCache: make(map[string]MatchResult),
revision: revision}
}
@@ -178,7 +174,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
return MatchResult{m, m, false}
}
pattern := request.pattern
passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
passMerger := PassMerger(&request.chunks, m.tac, request.revision)
if pattern.IsEmpty() {
return MatchResult{passMerger, passMerger, false}
}
@@ -216,7 +212,11 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort && request.pattern.sortable {
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches))
}
}
resultChan <- partialResult{idx, sliceMatches}
}(idx, m.slab[idx], chunks)
+23 -28
View File
@@ -10,46 +10,42 @@ func EmptyMerger(revision revision) *Merger {
// Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list
type Merger struct {
pattern *Pattern
lists [][]Result
merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
tac bool
final bool
count int
pass bool
startIndex int
revision revision
minIndex int32
maxIndex int32
pattern *Pattern
lists [][]Result
merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
tac bool
final bool
count int
pass bool
revision revision
minIndex int32
maxIndex int32
}
// PassMerger returns a new Merger that simply returns the items in the
// original order. startIndex items are skipped from the beginning.
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
// original order
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex, maxIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
}
si := int(startIndex)
mg := Merger{
pattern: nil,
chunks: chunks,
tac: tac,
count: 0,
pass: true,
startIndex: si,
revision: revision,
minIndex: minIndex + startIndex,
maxIndex: maxIndex}
pattern: nil,
chunks: chunks,
tac: tac,
count: 0,
pass: true,
revision: revision,
minIndex: minIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks {
mg.count += chunk.count
}
mg.count = max(0, mg.count-si)
return &mg
}
@@ -117,7 +113,6 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac {
idx = mg.count - idx - 1
}
idx += mg.startIndex
firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count
+11 -111
View File
@@ -8,7 +8,6 @@ import (
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/junegunn/fzf/src/algo"
@@ -96,7 +95,7 @@ Usage: fzf [options]
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
--highlight-line Highlight the whole current line
--cycle Enable cyclic scroll
--wrap[=MODE] Enable line wrap (char|word, default: char)
--wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0
--raw Enable raw mode (show non-matching items)
@@ -158,7 +157,7 @@ Usage: fzf [options]
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]]
[,[no]wrap[-word]][,[no]cycle][,[no]follow][,[no]info]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
[,[no]hidden][,border-STYLE]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
@@ -168,7 +167,6 @@ Usage: fzf [options]
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window
--preview-wrap-sign=STR Indicator for wrapped lines in the preview window
HEADER
--header=STR String to print as header
@@ -294,32 +292,14 @@ func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type trackOption struct {
enabled bool
index int32
}
type trackOption int
var (
trackDisabled = trackOption{false, minItem.Index()}
trackEnabled = trackOption{true, minItem.Index()}
const (
trackDisabled trackOption = iota
trackEnabled
trackCurrent
)
func (t trackOption) Disabled() bool {
return !t.enabled
}
func (t trackOption) Global() bool {
return t.enabled && t.index == minItem.Index()
}
func (t trackOption) Current() bool {
return t.enabled && t.index != minItem.Index()
}
func trackCurrent(index int32) trackOption {
return trackOption{true, index}
}
type windowPosition int
const (
@@ -369,7 +349,6 @@ type previewOpts struct {
scroll string
hidden bool
wrap bool
wrapWord bool
cycle bool
follow bool
info bool
@@ -546,7 +525,7 @@ func (o *previewOpts) compare(active *previewOpts, b *previewOpts) previewOptsCo
return previewOptsDifferentLayout
}
if a.wrap == b.wrap && a.wrapWord == b.wrapWord && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
if a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info && a.scroll == b.scroll {
return previewOptsSame
}
@@ -608,9 +587,7 @@ type Options struct {
Layout layoutType
Cycle bool
Wrap bool
WrapWord bool
WrapSign *string
PreviewWrapSign *string
MultiLine bool
CursorLine bool
KeepRight bool
@@ -678,8 +655,6 @@ type Options struct {
WalkerSkip []string
Version bool
Help bool
Threads int
Bench time.Duration
CPUProfile string
MEMProfile string
BlockProfile string
@@ -698,13 +673,7 @@ func filterNonEmpty(input []string) []string {
}
func defaultPreviewOpts(command string) previewOpts {
return previewOpts{
command: command,
position: posRight,
size: sizeSpec{50, true},
info: true,
border: defaultBorderShape,
}
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, defaultBorderShape, 0, 0, nil}
}
func defaultOptions() *Options {
@@ -746,7 +715,6 @@ func defaultOptions() *Options {
Layout: layoutDefault,
Cycle: false,
Wrap: false,
WrapWord: false,
MultiLine: true,
KeepRight: false,
Hscroll: true,
@@ -1421,14 +1389,6 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
cattr.Attr |= tui.Italic
case "underline":
cattr.Attr |= tui.Underline
case "underline-double":
cattr.Attr |= tui.Underline | tui.UlStyleDouble
case "underline-curly":
cattr.Attr |= tui.Underline | tui.UlStyleCurly
case "underline-dotted":
cattr.Attr |= tui.Underline | tui.UlStyleDotted
case "underline-dashed":
cattr.Attr |= tui.Underline | tui.UlStyleDashed
case "blink":
cattr.Attr |= tui.Blink
case "reverse":
@@ -1516,8 +1476,6 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
mergeAttr(&theme.Nomatch)
case "gutter":
mergeAttr(&theme.Gutter)
case "alt-gutter":
mergeAttr(&theme.AltGutter)
case "hl":
mergeAttr(&theme.Match)
case "current-hl", "hl+":
@@ -1629,7 +1587,7 @@ const (
func init() {
executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
@@ -1811,8 +1769,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleHeader)
case "toggle-wrap":
appendAction(actToggleWrap)
case "toggle-wrap-word":
appendAction(actToggleWrapWord)
case "toggle-multi-line":
appendAction(actToggleMultiLine)
case "toggle-hscroll":
@@ -1879,8 +1835,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actTogglePreview)
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-preview-wrap-word":
appendAction(actTogglePreviewWrapWord)
case "toggle-sort":
appendAction(actToggleSort)
case "offset-up":
@@ -2040,8 +1994,6 @@ func isExecuteAction(str string) actionType {
return actPreview
case "change-header":
return actChangeHeader
case "change-header-lines":
return actChangeHeaderLines
case "change-footer":
return actChangeFooter
case "change-list-label":
@@ -2102,8 +2054,6 @@ func isExecuteAction(str string) actionType {
return actTransformFooter
case "transform-header":
return actTransformHeader
case "transform-header-lines":
return actTransformHeaderLines
case "transform-ghost":
return actTransformGhost
case "transform-nth":
@@ -2134,8 +2084,6 @@ func isExecuteAction(str string) actionType {
return actBgTransformFooter
case "bg-transform-header":
return actBgTransformHeader
case "bg-transform-header-lines":
return actBgTransformHeaderLines
case "bg-transform-ghost":
return actBgTransformGhost
case "bg-transform-nth":
@@ -2298,13 +2246,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
opts.hidden = false
case "wrap":
opts.wrap = true
opts.wrapWord = false
case "wrap-word":
opts.wrap = true
opts.wrapWord = true
case "nowrap":
opts.wrap = false
opts.wrapWord = false
case "cycle":
opts.cycle = true
case "nocycle":
@@ -2868,29 +2811,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--no-cycle":
opts.Cycle = false
case "--wrap":
given, str := optionalNextString()
if given {
switch str {
case "char":
opts.Wrap = true
opts.WrapWord = false
case "word":
opts.Wrap = true
opts.WrapWord = true
default:
return errors.New("invalid wrap mode: " + str + " (expected: char or word)")
}
} else {
opts.Wrap = true
}
opts.Wrap = true
case "--no-wrap":
opts.Wrap = false
opts.WrapWord = false
case "--wrap-word":
opts.Wrap = true
opts.WrapWord = true
case "--no-wrap-word":
opts.WrapWord = false
case "--wrap-sign":
str, err := nextString("wrap sign required")
if err != nil {
@@ -3124,12 +3047,6 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--preview-wrap-sign":
str, err := nextString("preview wrap sign required")
if err != nil {
return err
}
opts.PreviewWrapSign = &str
case "--height":
str, err := nextString("height required: [~]HEIGHT[%]")
if err != nil {
@@ -3376,23 +3293,6 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
return err
}
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
case "--threads":
if opts.Threads, err = nextInt("number of threads required"); err != nil {
return err
}
if opts.Threads < 0 {
return errors.New("--threads must be a positive integer")
}
case "--bench":
str, err := nextString("duration required (e.g. 3s, 500ms)")
if err != nil {
return err
}
dur, err := time.ParseDuration(str)
if err != nil {
return errors.New("invalid duration for --bench: " + str)
}
opts.Bench = dur
case "--profile-cpu":
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
return err
-58
View File
@@ -448,64 +448,6 @@ func TestPreviewOpts(t *testing.T) {
opts.Preview.size.size == 70) {
t.Error(opts.Preview)
}
// wrap-word tests
opts = optsFor("--preview-window=wrap-word")
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
opts = optsFor("--preview-window=wrap-word,nowrap")
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
opts = optsFor("--preview-window=wrap-word,wrap")
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
}
}
func TestPreviewWrapSign(t *testing.T) {
// Default: no preview wrap sign override
opts := optsFor()
if opts.PreviewWrapSign != nil {
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
}
// --preview-wrap-sign sets PreviewWrapSign
opts = optsFor("--preview-wrap-sign", ">> ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign is independent of --wrap-sign
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
if opts.WrapSign == nil || *opts.WrapSign != "| " {
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign without --wrap-sign
opts = optsFor("--preview-wrap-sign", "→ ")
if opts.WrapSign != nil {
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
}
// Last --preview-wrap-sign wins
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
}
// Empty string is allowed
opts = optsFor("--preview-wrap-sign", "")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
}
}
func TestAdditiveExpect(t *testing.T) {
+19 -77
View File
@@ -64,9 +64,6 @@ type Pattern struct {
procFun map[termType]algo.Algo
cache *ChunkCache
denylist map[int32]struct{}
startIndex int32
directAlgo algo.Algo
directTerm *term
}
var _splitRegex *regexp.Regexp
@@ -77,7 +74,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
var asString string
if extended {
@@ -149,11 +146,9 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
delimiter: delimiter,
cache: cache,
denylist: denylist,
startIndex: startIndex,
procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey()
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -277,22 +272,6 @@ func (p *Pattern) buildCacheKey() string {
return strings.Join(cacheableTerms, "\t")
}
// buildDirectAlgo returns the algo function and term for the direct fast path
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
if !p.extended || len(p.nth) > 0 {
return nil, nil
}
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
t := &p.termSets[0][0]
if !t.inv && t.typ == termFuzzy {
return fuzzyAlgo, t
}
}
return nil, nil
}
// CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string {
return p.cacheKey
@@ -322,56 +301,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{}
// Skip header items in chunks that contain them
startIdx := 0
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
startIdx = int(p.startIndex - chunk.items[0].Index())
if startIdx >= chunk.count {
return matches
}
}
// Fast path: single fuzzy term, no nth, no denylist.
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
// and avoiding per-match []Offset heap allocation.
if p.directAlgo != nil && len(p.denylist) == 0 {
t := p.directTerm
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&chunk.items[idx].text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
&chunk.items[idx], res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
} else {
for _, result := range space {
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
&result.item.text, t.text, p.withPos, slab)
if res.Start >= 0 {
matches = append(matches, buildResultFromBounds(
result.item, res.Score,
int(res.Start), int(res.End), int(res.End), true))
}
}
}
return matches
}
if len(p.denylist) == 0 {
// Huge code duplication for minimizing unnecessary map lookups
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
matches = append(matches, match)
for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
matches = append(matches, match)
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
}
@@ -379,12 +320,12 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
}
if space == nil {
for idx := startIdx; idx < chunk.count; idx++ {
for idx := 0; idx < chunk.count; idx++ {
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
matches = append(matches, match)
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
@@ -392,29 +333,30 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
if _, prs := p.denylist[result.item.Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
matches = append(matches, match)
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
}
return matches
}
// MatchItem returns the match result if the Item is a match.
// A zero-value Result (with item == nil) indicates no match.
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if p.extended {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
return buildResult(item, offsets, bonus), offsets, pos
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
return Result{}, nil, nil
return nil, nil, nil
}
offset, bonus, pos := p.basicMatch(item, withPos, slab)
if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
return buildResult(item, offsets, bonus), offsets, pos
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
return Result{}, nil, nil
return nil, nil, nil
}
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
+1 -1
View File
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
}
func TestExact(t *testing.T) {
+3 -7
View File
@@ -178,7 +178,7 @@ func (r *Reader) feed(src io.Reader) {
var err error
for {
n := 0
scope := slab[:min(len(slab), readerBufferSize)]
scope := slab[:util.Min(len(slab), readerBufferSize)]
for range 100 {
n, err = src.Read(scope)
if n > 0 || err != nil {
@@ -303,12 +303,8 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
}
path = trimPath(path)
if path != "." {
isDirSymlink := isSymlinkToDir(path, de)
if isDirSymlink && !opts.follow {
return filepath.SkipDir
}
isDir := de.IsDir() || isDirSymlink
if isDir {
isDir := de.IsDir()
if isDir || opts.follow && isSymlinkToDir(path, de) {
base := filepath.Base(path)
if !opts.hidden && base[0] == '.' && base != ".." {
return filepath.SkipDir
+7 -88
View File
@@ -33,6 +33,8 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item}
numChars := item.text.Length()
minBegin := math.MaxUint16
minEnd := math.MaxUint16
maxEnd := 0
@@ -40,21 +42,13 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
for _, offset := range offsets {
b, e := int(offset[0]), int(offset[1])
if b < e {
minBegin = min(b, minBegin)
minEnd = min(e, minEnd)
maxEnd = max(e, maxEnd)
minBegin = util.Min(b, minBegin)
minEnd = util.Min(e, minEnd)
maxEnd = util.Max(e, maxEnd)
validOffsetFound = true
}
}
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
}
// buildResultFromBounds builds a Result from pre-computed offset bounds.
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
result := Result{item: item}
numChars := item.text.Length()
for idx, criterion := range sortCriteria {
val := uint16(math.MaxUint16)
switch criterion {
@@ -81,6 +75,7 @@ func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int,
val = item.TrimLength()
case byPathname:
if validOffsetFound {
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
lastDelim := -1
s := item.text.ToString()
for i := len(s) - 1; i >= 0; i-- {
@@ -211,7 +206,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
if bg == -1 {
bg = colBase.Bg()
}
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
}
var colors []colorOffset
add := func(idx int) {
@@ -339,79 +334,3 @@ func (a ByRelevanceTac) Swap(i, j int) {
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks(a[i], a[j], true)
}
// radixSortResults sorts Results by their points key using LSD radix sort.
// O(n) time complexity vs O(n log n) for comparison sort.
// The sort is stable, so equal-key items maintain original (item-index) order.
// For tac mode, runs of equal keys are reversed after sorting.
func radixSortResults(a []Result, tac bool, scratch []Result) []Result {
n := len(a)
if n < 128 {
if tac {
sort.Sort(ByRelevanceTac(a))
} else {
sort.Sort(ByRelevance(a))
}
return scratch[:0]
}
if cap(scratch) < n {
scratch = make([]Result, n)
}
buf := scratch[:n]
src, dst := a, buf
scattered := 0
for pass := range 8 {
shift := uint(pass) * 8
var count [256]int
for i := range src {
count[byte(sortKey(&src[i])>>shift)]++
}
// Skip if all items have the same byte value at this position
if count[byte(sortKey(&src[0])>>shift)] == n {
continue
}
var offset [256]int
for i := 1; i < 256; i++ {
offset[i] = offset[i-1] + count[i-1]
}
for i := range src {
b := byte(sortKey(&src[i]) >> shift)
dst[offset[b]] = src[i]
offset[b]++
}
src, dst = dst, src
scattered++
}
// If odd number of scatters, data is in buf, copy back to a
if scattered%2 == 1 {
copy(a, src)
}
// Handle tac: reverse runs of equal keys so equal-key items
// are in reverse item-index order
if tac {
i := 0
for i < n {
ki := sortKey(&a[i])
j := i + 1
for j < n && sortKey(&a[j]) == ki {
j++
}
if j-i > 1 {
for l, r := i, j-1; l < r; l, r = l+1, r-1 {
a[l], a[r] = a[r], a[l]
}
}
i = j
}
}
return scratch
}
+1 -5
View File
@@ -1,4 +1,4 @@
//go:build !386 && !amd64 && !arm64
//go:build !386 && !amd64
package fzf
@@ -14,7 +14,3 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}
func sortKey(r *Result) uint64 {
return uint64(r.points[0]) | uint64(r.points[1])<<16 | uint64(r.points[2])<<32 | uint64(r.points[3])<<48
}
+4 -62
View File
@@ -2,7 +2,6 @@ package fzf
import (
"math"
"math/rand"
"sort"
"testing"
@@ -125,10 +124,10 @@ func TestColorOffset(t *testing.T) {
item := Result{
item: &Item{
colors: &[]ansiOffset{
{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
@@ -183,60 +182,3 @@ func TestColorOffset(t *testing.T) {
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
}
}
func TestRadixSortResults(t *testing.T) {
sortCriteria = []criterion{byScore, byLength}
rng := rand.New(rand.NewSource(42))
for _, n := range []int{128, 256, 500, 1000} {
for _, tac := range []bool{false, true} {
// Build items with random points and indices
items := make([]*Item, n)
for i := range items {
items[i] = &Item{text: util.Chars{Index: int32(i)}}
}
results := make([]Result, n)
for i := range results {
results[i] = Result{
item: items[i],
points: [4]uint16{
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
uint16(rng.Intn(256)),
},
}
}
// Make some duplicates to test stability
for i := 0; i < n/4; i++ {
j := rng.Intn(n)
k := rng.Intn(n)
results[j].points = results[k].points
}
// Copy for reference sort
expected := make([]Result, n)
copy(expected, results)
if tac {
sort.Sort(ByRelevanceTac(expected))
} else {
sort.Sort(ByRelevance(expected))
}
// Radix sort
var scratch []Result
scratch = radixSortResults(results, tac, scratch)
for i := range results {
if results[i] != expected[i] {
t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
n, tac, i, results[i].item.Index(), expected[i].item.Index())
break
}
}
}
}
}
+1 -5
View File
@@ -1,4 +1,4 @@
//go:build 386 || amd64 || arm64
//go:build 386 || amd64
package fzf
@@ -14,7 +14,3 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}
func sortKey(r *Result) uint64 {
return *(*uint64)(unsafe.Pointer(&r.points[0]))
}
+12 -19
View File
@@ -183,22 +183,23 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
})
section := 0
var getMatch []string
Loop:
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0: // Request line
getMatch = getRegex.FindStringSubmatch(text)
if len(getMatch) == 0 && !strings.HasPrefix(text, "POST / HTTP") {
case 0:
getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
response := server.getHandler(parseGetParams(getMatch[1]))
if len(response) > 0 {
return good(response)
}
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1: // Request headers
if text == crlf { // End of headers
if len(getMatch) > 0 {
break Loop
}
case 1:
if text == crlf {
if contentLength == 0 {
return bad("content-length header missing")
}
@@ -218,7 +219,7 @@ Loop:
apiKey = strings.TrimSpace(pair[1])
}
}
case 2: // Request body
case 2:
body += text
}
}
@@ -227,14 +228,6 @@ Loop:
return unauthorized("invalid api key")
}
if len(getMatch) > 0 {
response := server.getHandler(parseGetParams(getMatch[1]))
if len(response) > 0 {
return good(response)
}
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
if len(body) < contentLength {
return bad("incomplete request")
}
+388 -722
View File
File diff suppressed because it is too large Load Diff
-69
View File
@@ -699,72 +699,3 @@ func readFile(path string) ([]byte, error) {
}
}
}
func TestWordWrapAnsiLine(t *testing.T) {
term := &Terminal{}
// Simple wrapping
result := term.wordWrapAnsiLine("hello world", 7, 2)
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
t.Errorf("Simple: %q", result)
}
// No wrapping needed
result = term.wordWrapAnsiLine("hello", 10, 2)
if len(result) != 1 || result[0] != "hello" {
t.Errorf("No wrap: %q", result)
}
// ANSI codes preserved across split
result = term.wordWrapAnsiLine("\x1b[31mhello \x1b[32mworld", 8, 2)
if len(result) != 2 || result[0] != "\x1b[31mhello" || result[1] != "\x1b[32mworld" {
t.Errorf("ANSI: %q", result)
}
// Long word (no space) — no break, let character wrapping handle it
result = term.wordWrapAnsiLine("abcdefghij", 5, 2)
if len(result) != 1 || result[0] != "abcdefghij" {
t.Errorf("Long word: %q", result)
}
// Multiple words with continuation wrapSignWidth
result = term.wordWrapAnsiLine("aa bb cc dd", 5, 2)
// max=5 for first line, max=3 for continuations (5-2)
// "aa bb" (5 wide), split at second space -> "aa bb" | "cc" | "dd"
if len(result) != 3 || result[0] != "aa bb" || result[1] != "cc" || result[2] != "dd" {
t.Errorf("Multiple words: %q", result)
}
// Empty string
result = term.wordWrapAnsiLine("", 10, 2)
if len(result) != 1 || result[0] != "" {
t.Errorf("Empty: %q", result)
}
// OSC 8 hyperlink preserved
result = term.wordWrapAnsiLine("\x1b]8;;http://example.com\x1b\\click here\x1b]8;;\x1b\\", 8, 2)
if len(result) != 2 {
t.Errorf("Hyperlink split count: %d, %q", len(result), result)
}
// Tab handling: tab expands to tabstop-aligned width
term.tabstop = 8
// "\thi there" — tab at column 0 expands to 8, total "hi" starts at 8
// maxWidth=15: "\thi" = 10 wide, "there" = 5 wide, total 16 > 15, wrap at space
result = term.wordWrapAnsiLine("\thi there", 15, 2)
if len(result) != 2 || result[0] != "\thi" || result[1] != "there" {
t.Errorf("Tab: %q", result)
}
// Tab as word boundary: "hello"(5) + tab(3→col8) + "world"(5) = 13 total
// maxWidth=13: fits without wrapping
result = term.wordWrapAnsiLine("hello\tworld", 13, 2)
if len(result) != 1 || result[0] != "hello\tworld" {
t.Errorf("Tab no wrap: %q", result)
}
// maxWidth=12: 13 > 12, wraps at tab
result = term.wordWrapAnsiLine("hello\tworld", 12, 2)
if len(result) != 2 || result[0] != "hello" || result[1] != "world" {
t.Errorf("Tab wrap: %q", result)
}
}
+1 -1
View File
@@ -302,7 +302,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
end += numTokens + 1
}
}
minIdx = max(0, begin-1)
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
parts = append(parts, tokens[idx-1].text)
+5 -5
View File
@@ -34,11 +34,11 @@ func (r *FullscreenRenderer) ShowCursor() {}
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) GetChar(bool) Event { return Event{} }
func (r *FullscreenRenderer) CancelGetChar() {}
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
+72 -104
View File
@@ -13,6 +13,7 @@ import (
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
)
@@ -25,7 +26,6 @@ const (
escPollInterval = 5
offsetPollTries = 10
maxInputBuffer = 1024 * 1024
maxSelectTries = 100
)
const DefaultTtyDevice string = "/dev/tty"
@@ -49,18 +49,6 @@ const DIM string = "\x1b[2m"
const CR string = DIM + "␍"
const LF string = DIM + "␊"
type getCharResult int
const (
getCharSuccess getCharResult = iota
getCharError
getCharCancelled
)
func (r getCharResult) ok() bool {
return r == getCharSuccess
}
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str)
runes := []rune{}
@@ -116,7 +104,6 @@ type LightRenderer struct {
clicks [][2]int
ttyin *os.File
ttyout *os.File
cancel func()
buffer []byte
origState *term.State
width int
@@ -131,9 +118,9 @@ type LightRenderer struct {
x int
maxHeightFunc func(int) int
showCursor bool
mutex sync.Mutex
// Windows only
mutex sync.Mutex
ttyinChannel chan byte
inHandle uintptr
outHandle uintptr
@@ -206,6 +193,11 @@ func (r *LightRenderer) Init() error {
if r.fullscreen {
r.smcup()
} else {
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
y, x := r.findOffset()
r.mouse = r.mouse && y >= 0
// When --no-clear is used for repetitive relaunching, there is a small
@@ -216,11 +208,6 @@ func (r *LightRenderer) Init() error {
r.upOneLine = true
r.makeSpace()
}
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
for i := 1; i < r.MaxY(); i++ {
r.makeSpace()
}
@@ -275,18 +262,16 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
return r.getBytesInternal(cancellable, r.buffer, false)
func (r *LightRenderer) getBytes() ([]byte, error) {
bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
}
func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) {
c, result := r.getch(cancellable, nonblock)
if result == getCharCancelled {
return buffer, getCharCancelled, nil
}
if !nonblock && !result.ok() {
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice)
return nil, errors.New("failed to read " + DefaultTtyDevice)
}
retries := 0
@@ -297,8 +282,8 @@ func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblo
pc := c
for {
c, result = r.getch(false, true)
if !result.ok() {
c, ok = r.getch(true)
if !ok {
if retries > 0 {
retries--
time.Sleep(escPollInterval * time.Millisecond)
@@ -317,24 +302,20 @@ func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblo
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
return nil, getCharError, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
return buffer, getCharSuccess, nil
return buffer, nil
}
func (r *LightRenderer) GetChar(cancellable bool) Event {
func (r *LightRenderer) GetChar() Event {
var err error
var result getCharResult
if len(r.buffer) == 0 {
r.buffer, result, err = r.getBytes(cancellable)
r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
}
if len(r.buffer) == 0 {
return Event{Fatal, 0, nil}
@@ -370,14 +351,9 @@ func (r *LightRenderer) GetChar(cancellable bool) Event {
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer, result, err = r.getBytes(true)
if err != nil {
if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
if result == getCharCancelled {
return Event{Invalid, 0, nil}
}
ev = r.escSequence(&sz)
}
return ev
@@ -395,21 +371,6 @@ func (r *LightRenderer) GetChar(cancellable bool) Event {
return Event{Rune, char, nil}
}
func (r *LightRenderer) CancelGetChar() {
r.mutex.Lock()
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
r.mutex.Unlock()
}
func (r *LightRenderer) setCancel(f func()) {
r.mutex.Lock()
r.cancel = f
r.mutex.Unlock()
}
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{Esc, 0, nil}
@@ -1072,8 +1033,8 @@ func (r *LightRenderer) MaxY() int {
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = max(0, width)
height = max(0, height)
width = util.Max(0, width)
height = util.Max(0, height)
w := &LightWindow{
renderer: r,
colored: r.theme.Colored,
@@ -1322,18 +1283,7 @@ func attrCodes(attr Attr) []string {
codes = append(codes, "3")
}
if (attr & Underline) > 0 {
switch attr.UnderlineStyle() {
case UlStyleDouble:
codes = append(codes, "4:2")
case UlStyleCurly:
codes = append(codes, "4:3")
case UlStyleDotted:
codes = append(codes, "4:4")
case UlStyleDashed:
codes = append(codes, "4:5")
default:
codes = append(codes, "4")
}
codes = append(codes, "4")
}
if (attr & Blink) > 0 {
codes = append(codes, "5")
@@ -1371,27 +1321,8 @@ func colorCodes(fg Color, bg Color) []string {
return codes
}
func ulColorCode(c Color) string {
if c == colDefault {
return ""
}
if c.is24() {
r := (c >> 16) & 0xff
g := (c >> 8) & 0xff
b := (c) & 0xff
return fmt.Sprintf("58;2;%d;%d;%d", r, g, b)
}
if c >= 0 && c < 256 {
return fmt.Sprintf("58;5;%d", c)
}
return ""
}
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
if ulCode := ulColorCode(ul); ulCode != "" {
codes = append(codes, ulCode)
}
code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0, code
}
@@ -1405,28 +1336,65 @@ func cleanse(str string) string {
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false, code)
w.csi("0m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
hasColors, code := w.csiColor(fg, bg, attr)
if hasColors {
defer w.csi("0m")
}
w.stderrInternal(cleanse(text), false, code)
}
type wrappedLine struct {
text string
displayWidth int
}
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
lines := []wrappedLine{}
width := 0
line := ""
gr := uniseg.NewGraphemes(input)
max := initialMax
for gr.Next() {
rs := gr.Runes()
str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else {
w = uniseg.StringWidth(str)
}
width += w
if prefixLength+width <= max {
line += str
} else {
lines = append(lines, wrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = w
max = initialMax - wrapSignWidth
}
}
lines = append(lines, wrappedLine{string(line), width})
return lines
}
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
allLines := strings.Split(str, "\n")
for i, line := range allLines {
lines := WrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
for j, wl := range lines {
if w.posx < w.width {
w.stderrInternal(wl.Text, false, resetCode)
w.posx += wl.DisplayWidth
}
w.stderrInternal(wl.text, false, resetCode)
w.posx += wl.displayWidth
// Wrap line
if j < len(lines)-1 || i < len(allLines)-1 {
@@ -1464,7 +1432,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
func (w *LightWindow) setBg() string {
if w.bg != colDefault {
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
return code
}
// Should clear dim attribute after ␍ in the preview window
@@ -1486,7 +1454,7 @@ func (w *LightWindow) Fill(text string) FillReturn {
return w.fill(text, code)
}
func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn {
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
@@ -1494,7 +1462,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string
if bg == colDefault {
bg = w.bg
}
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
defer w.csi("0m")
return w.fill(text, resetCode)
}
+1 -18
View File
@@ -15,27 +15,10 @@ func TestLightRenderer(t *testing.T) {
light_renderer := renderer.(*LightRenderer)
go func() {
for {
light_renderer.mutex.Lock()
ready := light_renderer.cancel != nil
light_renderer.mutex.Unlock()
if ready {
light_renderer.CancelGetChar()
break
}
}
}()
event := light_renderer.GetChar(true)
if event.Type != Invalid {
t.Error("Not cancelled")
}
assertCharSequence := func(sequence string, name string) {
bytes := []byte(sequence)
light_renderer.buffer = bytes
event := light_renderer.GetChar(true)
event := light_renderer.GetChar()
if event.KeyName() != name {
t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s",
+7 -54
View File
@@ -99,7 +99,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
var err error
bytes := []byte{}
for tries := range offsetPollTries {
bytes, _, err = r.getBytesInternal(false, bytes, tries > 0)
bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
@@ -114,62 +114,15 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return -1, -1
}
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
b := make([]byte, 1)
fd := r.fd()
getter := func() (int, getCharResult) {
b := make([]byte, 1)
util.SetNonblock(r.ttyin, nonblock)
_, err := util.Read(fd, b)
if err != nil {
return 0, getCharError
}
return int(b[0]), getCharSuccess
}
if nonblock || !cancellable {
return getter()
}
rpipe, wpipe, err := os.Pipe()
util.SetNonblock(r.ttyin, nonblock)
_, err := util.Read(fd, b)
if err != nil {
// Fallback to blocking read without cancellation
return getter()
return 0, false
}
r.setCancel(func() {
wpipe.Write([]byte{0})
})
defer func() {
r.setCancel(nil)
rpipe.Close()
wpipe.Close()
}()
cancelFd := int(rpipe.Fd())
for range maxSelectTries {
var rfds unix.FdSet
limit := len(rfds.Bits) * unix.NFDBITS
if fd >= limit || cancelFd >= limit {
return getter()
}
rfds.Set(fd)
rfds.Set(cancelFd)
_, err := unix.Select(max(fd, cancelFd)+1, &rfds, nil, nil, nil)
if err != nil {
if err == syscall.EINTR {
continue
}
return 0, getCharError
}
if rfds.IsSet(cancelFd) {
return 0, getCharCancelled
}
if rfds.IsSet(fd) {
return getter()
}
}
return 0, getCharError
return int(b[0]), true
}
func (r *LightRenderer) Size() TermSize {
+10 -27
View File
@@ -151,33 +151,16 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
}
func (r *LightRenderer) getch(cancellable bool, nonblock bool) (int, getCharResult) {
if !nonblock && !cancellable {
bc := <-r.ttyinChannel
return int(bc), getCharSuccess
}
var timeout <-chan time.Time
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
if nonblock {
timeout = time.After(timeoutInterval * time.Millisecond)
}
var cancel chan struct{}
if cancellable {
cancel = make(chan struct{})
r.setCancel(func() {
close(cancel)
})
defer r.setCancel(nil)
}
select {
case bc := <-r.ttyinChannel:
return int(bc), getCharSuccess
case <-cancel:
return 0, getCharCancelled
case <-timeout:
// NOTE: not really an error
return 0, getCharError
select {
case bc := <-r.ttyinChannel:
return int(bc), true
case <-time.After(timeoutInterval * time.Millisecond):
return 0, false
}
} else {
bc := <-r.ttyinChannel
return int(bc), true
}
}
+56 -92
View File
@@ -5,7 +5,6 @@ package tui
import (
"os"
"regexp"
"strings"
"time"
"github.com/gdamore/tcell/v2"
@@ -54,7 +53,6 @@ type TcellWindow struct {
showCursor bool
wrapSign string
wrapSignWidth int
tabstop int
}
func (w *TcellWindow) Top() int {
@@ -248,7 +246,7 @@ func (r *FullscreenRenderer) Size() TermSize {
return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventPaste:
@@ -705,10 +703,6 @@ func (r *FullscreenRenderer) GetChar(cancellable bool) Event {
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) CancelGetChar() {
// TODO
}
func (r *FullscreenRenderer) Pause(clear bool) {
if clear {
_screen.Suspend()
@@ -735,8 +729,8 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
width = max(0, width)
height = max(0, height)
width = util.Max(0, width)
height = util.Max(0, height)
normal := ColBorder
switch windowType {
case WindowList:
@@ -759,8 +753,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height,
normal: normal,
borderStyle: borderStyle,
showCursor: r.showCursor,
tabstop: r.tabstop}
showCursor: r.showCursor}
w.Erase()
return w
}
@@ -828,21 +821,6 @@ func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
return style
}
func underlineStyleFromAttr(a Attr) tcell.UnderlineStyle {
switch a.UnderlineStyle() {
case UlStyleDouble:
return tcell.UnderlineStyleDouble
case UlStyleCurly:
return tcell.UnderlineStyleCurly
case UlStyleDotted:
return tcell.UnderlineStyleDotted
case UlStyleDashed:
return tcell.UnderlineStyleDashed
default:
return tcell.UnderlineStyleSolid
}
}
func (w *TcellWindow) printString(text string, pair ColorPair) {
lx := 0
a := pair.Attr()
@@ -851,18 +829,11 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
if a&AttrClear == 0 {
style = style.
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0).
Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
}
style = w.withUrl(style)
@@ -897,8 +868,10 @@ func (w *TcellWindow) CPrint(pair ColorPair, text string) {
w.printString(text, pair)
}
func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
lx := 0
a := pair.Attr()
var style tcell.Style
if w.color {
style = pair.style()
@@ -910,73 +883,64 @@ func (w *TcellWindow) pairStyle(pair ColorPair) tcell.Style {
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
return w.withUrl(style)
}
style = w.withUrl(style)
func (w *TcellWindow) renderGraphemes(text string, style tcell.Style) {
gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() {
st := style
rs := gr.Runes()
if len(rs) == 1 && rs[0] == '\r' {
st = style.Dim(true)
rs[0] = '␍'
}
xPos := w.left + w.lastX
yPos := w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
}
w.lastX += util.StringWidth(string(rs))
}
}
func (w *TcellWindow) renderWrapSign(style tcell.Style) {
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
gr := uniseg.NewGraphemes(sign)
for gr.Next() {
rs := gr.Runes()
_screen.SetContent(w.left+w.lastX, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
w.lastX += uniseg.StringWidth(string(rs))
}
}
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
style := w.pairStyle(pair)
for i, segment := range strings.Split(text, "\n") {
for j, wl := range WrapLine(segment, w.lastX, w.width, w.tabstop, w.wrapSignWidth) {
if i > 0 || j > 0 {
if len(rs) == 1 {
r := rs[0]
switch r {
case '\r':
st = style.Dim(true)
rs[0] = '␍'
case '\n':
w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0
if j > 0 {
w.renderWrapSign(style)
}
}
if w.lastX < w.width {
w.renderGraphemes(wl.Text, style)
lx = 0
continue Loop
}
}
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= w.left+w.width {
w.lastY++
if w.lastY >= w.height {
return FillSuspend
}
w.lastX = 0
lx = 0
xPos = w.left
sign := w.wrapSign
if w.wrapSignWidth > w.width {
runes, _ := util.Truncate(sign, w.width)
sign = string(runes)
}
wgr := uniseg.NewGraphemes(sign)
for wgr.Next() {
rs := wgr.Runes()
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
lx += uniseg.StringWidth(string(rs))
}
xPos = w.left + lx
}
yPos := w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
lx += util.StringWidth(string(rs))
}
if w.lastX >= w.width {
w.lastX += lx
if w.lastX == w.width {
w.lastY++
w.lastX = 0
return FillNextLine
@@ -999,14 +963,14 @@ func (w *TcellWindow) LinkEnd() {
w.params = nil
}
func (w *TcellWindow) CFill(fg Color, bg Color, ul Color, a Attr, str string) FillReturn {
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = w.normal.Fg()
}
if bg == colDefault {
bg = w.normal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg, a).WithUl(ul))
return w.fillString(str, NewColorPair(fg, bg, a))
}
func (w *TcellWindow) DrawBorder() {
+6 -6
View File
@@ -253,7 +253,7 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
}
r := NewFullscreenRenderer(&ColorTheme{}, false, false, 8)
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
r.Init()
// run and evaluate the tests
@@ -265,22 +265,22 @@ func TestGetCharEventKey(t *testing.T) {
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
// process the event in fzf and evaluate the test
gotEvent := r.GetChar(true)
gotEvent := r.GetChar()
// skip Resize events, those are sometimes put in the buffer outside of this test
if initialResizeAsInvalid && gotEvent.Type == Invalid {
t.Logf("Resize as Invalid swallowed")
initialResizeAsInvalid = false
gotEvent = r.GetChar(true)
gotEvent = r.GetChar()
}
if gotEvent.Type == Resize {
t.Logf("Resize swallowed")
gotEvent = r.GetChar(true)
gotEvent = r.GetChar()
}
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
assert(t, "r.GetChar(true).Type", gotEvent.Type, test.wantKey.Type)
assert(t, "r.GetChar(true).Char", gotEvent.Char, test.wantKey.Char)
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
}
r.Close()
+9 -100
View File
@@ -2,7 +2,6 @@ package tui
import (
"strconv"
"strings"
"time"
"github.com/junegunn/fzf/src/util"
@@ -18,34 +17,15 @@ const (
BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Strip = Attr(1 << 12)
// Underline style stored in bits 13-15 (3 bits, values 0-4)
// Only meaningful when the Underline attribute bit is also set.
// 0 = solid (default)
UnderlineStyleShift = 13
UnderlineStyleMask = Attr(0b111 << UnderlineStyleShift)
UlStyleDouble = Attr(0b001 << UnderlineStyleShift)
UlStyleCurly = Attr(0b010 << UnderlineStyleShift)
UlStyleDotted = Attr(0b011 << UnderlineStyleShift)
UlStyleDashed = Attr(0b100 << UnderlineStyleShift)
)
func (a Attr) UnderlineStyle() Attr {
return a & UnderlineStyleMask
}
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return (b &^ AttrRegular) | (a & BoldForce)
}
merged := (a &^ AttrRegular) | b
// When b sets Underline, use b's underline style instead of OR'ing
if b&Underline > 0 {
merged = (merged &^ UnderlineStyleMask) | (b & UnderlineStyleMask)
}
return merged
return (a &^ AttrRegular) | b
}
// Types of user action
@@ -372,7 +352,6 @@ const (
type ColorPair struct {
fg Color
bg Color
ul Color
attr Attr
}
@@ -384,11 +363,11 @@ func HexToColor(rrggbb string) Color {
}
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
return ColorPair{fg, bg, colDefault, attr}
return ColorPair{fg, bg, attr}
}
func NoColorPair() ColorPair {
return ColorPair{-1, -1, -1, 0}
return ColorPair{-1, -1, 0}
}
func (p ColorPair) Fg() Color {
@@ -399,16 +378,6 @@ func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) Ul() Color {
return p.ul
}
func (p ColorPair) WithUl(ul Color) ColorPair {
dup := p
dup.ul = ul
return dup
}
func (p ColorPair) Attr() Attr {
return p.attr
}
@@ -435,9 +404,6 @@ func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
if other.bg != except {
dup.bg = other.bg
}
if other.ul != except {
dup.ul = other.ul
}
return dup
}
@@ -449,13 +415,13 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
dup := p
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
fgPair := ColorPair{fg.Color, colUndefined, fg.Attr}
return dup.Merge(fgPair)
}
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
dup := p
bgPair := ColorPair{colUndefined, bg.Color, colUndefined, bg.Attr}
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
return dup.Merge(bgPair)
}
@@ -490,7 +456,6 @@ type ColorTheme struct {
PreviewBg ColorAttr
DarkBg ColorAttr
Gutter ColorAttr
AltGutter ColorAttr
Prompt ColorAttr
InputBg ColorAttr
InputBorder ColorAttr
@@ -783,8 +748,7 @@ type Renderer interface {
HideCursor()
ShowCursor()
GetChar(cancellable bool) Event
CancelGetChar()
GetChar() Event
Top() int
MaxX() int
@@ -817,7 +781,7 @@ type Window interface {
Print(text string)
CPrint(color ColorPair, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
LinkBegin(uri string, params string)
LinkEnd()
Erase()
@@ -830,18 +794,16 @@ type FullscreenRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
tabstop int
prevDownTime time.Time
clicks [][2]int
showCursor bool
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int) Renderer {
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
r := &FullscreenRenderer{
theme: theme,
mouse: mouse,
forceBlack: forceBlack,
tabstop: tabstop,
prevDownTime: time.Unix(0, 0),
clicks: [][2]int{},
showCursor: true}
@@ -864,8 +826,6 @@ var (
ColCursor ColorPair
ColCursorEmpty ColorPair
ColCursorEmptyChar ColorPair
ColAltCursorEmpty ColorPair
ColAltCursorEmptyChar ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
@@ -931,7 +891,6 @@ func init() {
PreviewFg: defaultColor,
PreviewBg: defaultColor,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: defaultColor,
PreviewScrollbar: defaultColor,
PreviewLabel: defaultColor,
@@ -984,7 +943,6 @@ func init() {
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
@@ -1033,7 +991,6 @@ func init() {
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
@@ -1084,7 +1041,6 @@ func init() {
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
@@ -1135,7 +1091,6 @@ func init() {
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
@@ -1253,7 +1208,6 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
gutter.Attr = Dim
}
theme.Gutter = o(theme.DarkBg, gutter)
theme.AltGutter = o(theme.Gutter, theme.AltGutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
@@ -1307,7 +1261,7 @@ func initPalette(theme *ColorTheme) {
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
bg.Color = colDefault
}
return ColorPair{fg.Color, bg.Color, colDefault, fg.Attr}
return ColorPair{fg.Color, bg.Color, fg.Attr}
}
blank := theme.ListFg
blank.Attr = AttrRegular
@@ -1323,8 +1277,6 @@ func initPalette(theme *ColorTheme) {
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltCursorEmpty = pair(blank, theme.AltGutter)
ColAltCursorEmptyChar = pair(theme.AltGutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
@@ -1363,46 +1315,3 @@ func initPalette(theme *ColorTheme) {
func runeWidth(r rune) int {
return uniseg.StringWidth(string(r))
}
// WrappedLine represents a single visual line after character-level wrapping.
type WrappedLine struct {
Text string
DisplayWidth int
}
// WrapLine splits a single line (no embedded \n) into visual lines
// that fit within initialMax columns. Character-level wrapping only.
func WrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []WrappedLine {
lines := []WrappedLine{}
width := 0
line := ""
gr := uniseg.NewGraphemes(input)
maxWidth := initialMax
contMax := max(1, initialMax-wrapSignWidth)
for gr.Next() {
rs := gr.Runes()
str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = strings.Repeat(" ", w)
} else if rs[0] == '\r' {
w++
} else {
w = uniseg.StringWidth(str)
}
width += w
if prefixLength+width <= maxWidth {
line += str
} else {
lines = append(lines, WrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = w
maxWidth = contMax
}
}
lines = append(lines, WrappedLine{string(line), width})
return lines
}
-40
View File
@@ -2,46 +2,6 @@ package tui
import "testing"
func TestWrapLine(t *testing.T) {
// Basic wrapping
lines := WrapLine("hello world", 0, 7, 8, 2)
if len(lines) != 2 || lines[0].Text != "hello w" || lines[1].Text != "orld" {
t.Errorf("Basic wrap: %v", lines)
}
// Exact fit — no wrapping needed
lines = WrapLine("hello", 0, 5, 8, 2)
if len(lines) != 1 || lines[0].Text != "hello" || lines[0].DisplayWidth != 5 {
t.Errorf("Exact fit: %v", lines)
}
// With prefix length
lines = WrapLine("hello", 3, 5, 8, 2)
if len(lines) != 2 || lines[0].Text != "he" || lines[1].Text != "llo" {
t.Errorf("Prefix length: %v", lines)
}
// Empty string
lines = WrapLine("", 0, 10, 8, 2)
if len(lines) != 1 || lines[0].Text != "" || lines[0].DisplayWidth != 0 {
t.Errorf("Empty string: %v", lines)
}
// Continuation lines account for wrapSignWidth
lines = WrapLine("abcdefghij", 0, 5, 8, 2)
// First line: "abcde" (5 chars fit in width 5)
// Continuation max: 5-2=3, so "fgh" then "ij"
if len(lines) != 3 || lines[0].Text != "abcde" || lines[1].Text != "fgh" || lines[2].Text != "ij" {
t.Errorf("Continuation: %v", lines)
}
// Tab expansion
lines = WrapLine("\there", 0, 10, 4, 2)
if len(lines) != 1 || lines[0].DisplayWidth != 8 {
t.Errorf("Tab: %v", lines)
}
}
func TestHexToColor(t *testing.T) {
assert := func(expr string, r, g, b int) {
color := HexToColor(expr)
+2 -15
View File
@@ -187,7 +187,7 @@ func (chars *Chars) TrailingWhitespaces() int {
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
whitespaces := chars.TrailingWhitespaces()
end := len(chars.slice) - whitespaces
chars.slice = chars.slice[0:max(end, maxIndex)]
chars.slice = chars.slice[0:Max(end, maxIndex)]
}
func (chars *Chars) TrimSuffix(runes []rune) {
@@ -249,7 +249,7 @@ func (chars *Chars) Prepend(prefix string) {
}
}
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, wrapWord bool) ([][]rune, bool) {
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
text := make([]rune, chars.Length())
copy(text, chars.ToRunes())
@@ -307,19 +307,6 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
if overflowIdx == 0 {
overflowIdx = 1
}
if wrapWord {
// Find last space/tab at or before overflowIdx
breakIdx := -1
for k := overflowIdx; k > 0; k-- {
if line[k-1] == ' ' || line[k-1] == '\t' {
breakIdx = k
break
}
}
if breakIdx > 0 {
overflowIdx = breakIdx
}
}
if len(wrapped) >= maxLines {
return wrapped, true
}
+1 -49
View File
@@ -51,7 +51,7 @@ func TestTrimLength(t *testing.T) {
func TestCharsLines(t *testing.T) {
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop, false)
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
@@ -81,51 +81,3 @@ func TestCharsLines(t *testing.T) {
// With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false)
}
func TestCharsLinesWrapWord(t *testing.T) {
// "hello world foo bar" with width 12 should break at word boundaries
chars := ToChars([]byte("hello world foo bar"))
lines, overflow := chars.Lines(false, 100, 12, 0, 8, true)
// "hello world " (12) | "foo bar" (7)
if len(lines) != 2 || overflow {
t.Errorf("Expected 2 lines, got %d (overflow: %v): %v", len(lines), overflow, lines)
}
if string(lines[0]) != "hello world " {
t.Errorf("Expected first line 'hello world ', got %q", string(lines[0]))
}
if string(lines[1]) != "foo bar" {
t.Errorf("Expected second line 'foo bar', got %q", string(lines[1]))
}
// No word boundary: a single long word falls back to character wrap
chars2 := ToChars([]byte("abcdefghijklmnop"))
lines2, _ := chars2.Lines(false, 100, 10, 0, 8, true)
if len(lines2) != 2 {
t.Errorf("Expected 2 lines for long word, got %d: %v", len(lines2), lines2)
}
if string(lines2[0]) != "abcdefghij" {
t.Errorf("Expected first line 'abcdefghij', got %q", string(lines2[0]))
}
// Tab as word boundary
chars3 := ToChars([]byte("hello\tworld"))
lines3, _ := chars3.Lines(false, 100, 7, 0, 8, true)
// "hello\t" should break at tab (width of tab at pos 5 with tabstop 8 = 3, total width = 8 > 7)
// Actually RunesWidth: 'h'=1,'e'=1,'l'=1,'l'=1,'o'=1,'\t'=3 = 8 > 7, overflowIdx=5
// Then word-wrap scans back and finds no space/tab before idx 5 (tab IS at idx 5 but we check line[k-1])
// Wait - let me think: overflowIdx=5, we check k=5 -> line[4]='o', k=4 -> line[3]='l'... no space/tab found
// Falls back to character wrap: "hello" | "\tworld"
if len(lines3) < 2 {
t.Errorf("Expected at least 2 lines for tab test, got %d: %v", len(lines3), lines3)
}
// wrapWord=false still character-wraps
chars4 := ToChars([]byte("hello world"))
lines4, _ := chars4.Lines(false, 100, 8, 0, 8, false)
if len(lines4) != 2 {
t.Errorf("Expected 2 lines with wrapWord=false, got %d: %v", len(lines4), lines4)
}
if string(lines4[0]) != "hello wo" {
t.Errorf("Expected first line 'hello wo', got %q", string(lines4[0]))
}
}
+63 -10
View File
@@ -1,11 +1,11 @@
package util
import (
"cmp"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/mattn/go-isatty"
"github.com/rivo/uniseg"
@@ -18,13 +18,8 @@ func StringWidth(s string) int {
// RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
return StringsWidth(string(runes), prefixWidth, tabstop, limit)
}
// StringsWidth returns the width of the string
func StringsWidth(str string, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0
gr := uniseg.NewGraphemes(str)
gr := uniseg.NewGraphemes(string(runes))
idx := 0
for gr.Next() {
rs := gr.Runes()
@@ -60,8 +55,54 @@ func Truncate(input string, limit int) ([]rune, int) {
return runes, width
}
func Constrain[T cmp.Ordered](val, minimum, maximum T) T {
return max(min(val, maximum), minimum)
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {
return first
}
return second
}
// Max16 returns the largest integer
func Max16(first int16, second int16) int16 {
if first >= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
}
// Min returns the smallest integer
func Min(first int, second int) int {
if first <= second {
return first
}
return second
}
// Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
}
return second
}
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 {
return Max32(Min32(val, max), min)
}
// Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int {
return Max(Min(val, max), min)
}
func AsUint16(val int) uint16 {
@@ -73,6 +114,18 @@ func AsUint16(val int) uint16 {
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration {
if val < min {
return min
}
if val > max {
return max
}
return val
}
// IsTty returns true if the file is a terminal
func IsTty(file *os.File) bool {
fd := file.Fd()
@@ -144,7 +197,7 @@ func CompareVersions(v1, v2 string) int {
return n
}
for i := 0; i < max(len(parts1), len(parts2)); i++ {
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
var p1, p2 int
if i < len(parts1) {
p1 = atoi(parts1[i])
+92
View File
@@ -4,8 +4,72 @@ import (
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 {
t.Error("Expected", 5)
}
}
func TestMax16(t *testing.T) {
if Max16(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max16(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
t.Error("Expected", math.MaxInt16)
}
if Max16(0, math.MinInt16) != 0 {
t.Error("Expected", 0)
}
}
func TestMax32(t *testing.T) {
if Max32(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max32(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
t.Error("Expected", math.MaxInt32)
}
if Max32(0, math.MinInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestMin(t *testing.T) {
if Min(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min(-2, 5) != -2 {
t.Error("Expected", -2)
}
}
func TestMin32(t *testing.T) {
if Min32(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min32(-2, 5) != -2 {
t.Error("Expected", -2)
}
if Min32(math.MaxInt32, 0) != 0 {
t.Error("Expected", 0)
}
if Min32(0, math.MinInt32) != math.MinInt32 {
t.Error("Expected", math.MinInt32)
}
}
func TestConstrain(t *testing.T) {
if Constrain(-3, -1, 3) != -1 {
t.Error("Expected", -1)
@@ -19,6 +83,22 @@ func TestConstrain(t *testing.T) {
}
}
func TestConstrain32(t *testing.T) {
if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1)
}
if Constrain32(2, -1, 3) != 2 {
t.Error("Expected", 2)
}
if Constrain32(5, -1, 3) != 3 {
t.Error("Expected", 3)
}
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestAsUint16(t *testing.T) {
if AsUint16(5) != 5 {
t.Error("Expected", 5)
@@ -40,6 +120,18 @@ func TestAsUint16(t *testing.T) {
}
}
func TestDurWithIn(t *testing.T) {
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
t.Error("Expected", time.Duration(0))
}
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
t.Error("Expected", time.Second)
}
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
t.Error("Expected", time.Second)
}
}
func TestOnce(t *testing.T) {
o := Once(false)
if o() {
-17
View File
@@ -1,17 +0,0 @@
# Unset fzf variables
set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_TMUX_OPTS
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
set -e FZF_API_KEY
# Unset completion-specific variables
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
set -gx FZF_COMPLETION_TRIGGER '++'
set -gx fish_history fzf_test
# Add fzf to PATH
fish_add_path <%= BASE %>/bin
# Source key bindings and completion
source "<%= BASE %>/shell/key-bindings.fish"
source "<%= BASE %>/shell/completion.fish"
+1 -12
View File
@@ -11,7 +11,6 @@ require 'net/http'
require 'json'
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
FISH_TEMPLATE = File.read(File.expand_path('common.fish', __dir__))
UNSETS = %w[
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
FZF_TMUX FZF_TMUX_OPTS
@@ -67,16 +66,7 @@ class Shell
end
def fish
@fish ||=
begin
confdir = '/tmp/fzf-fish'
FileUtils.rm_rf(confdir)
FileUtils.mkdir_p("#{confdir}/fish/conf.d")
File.open("#{confdir}/fish/conf.d/fzf.fish", 'w') do |f|
f.puts ERB.new(FISH_TEMPLATE).result(binding)
end
"rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"
end
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
end
end
end
@@ -163,7 +153,6 @@ class Tmux
self.until(true) do |lines|
message = "Prepare[#{tries}]"
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
sleep(0.15)
lines[-1] == message
end
rescue Minitest::Assertion
+11 -114
View File
@@ -1191,7 +1191,7 @@ class TestCore < TestInteractive
end
def test_freeze_left_keep_right
tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter
tmux.send_keys %[seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line], :Enter
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
tmux.send_keys '5'
tmux.until { |lines| assert_match(/^> 1␊2␊3␊4␊5␊.*XX$/, lines[-3]) }
@@ -1202,21 +1202,21 @@ class TestCore < TestInteractive
end
def test_freeze_left_and_right
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX), :Enter
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX], :Enter
tmux.until { |lines| assert_match(/XX9998 9999 10000$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 2 3XX.*XX9998 9999 10000$/, lines[-3]) }
tmux.until { |lines| assert_match(/^> 1 2 3XX.*XX9998 9999 10000$/,lines[-3]) }
end
def test_freeze_left_and_right_delimiter
tmux.send_keys %(seq 10000 | tr "\n" ' ' | sed 's/ / , /g' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX --delimiter ' , '), :Enter
tmux.send_keys %[seq 10000 | tr "\n" ' ' | sed 's/ / , /g' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX --delimiter ' , '], :Enter
tmux.until { |lines| assert_match(/XX, 9999 , 10000 ,$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 , 2 , 3 ,XX.*XX, 9999 , 10000 ,$/, lines[-3]) }
tmux.until { |lines| assert_match(/^> 1 , 2 , 3 ,XX.*XX, 9999 , 10000 ,$/,lines[-3]) }
end
def test_freeze_right_exceed_range
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-right 100000 --ellipsis XX), :Enter
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-right 100000 --ellipsis XX], :Enter
['', "'1000"].each do |query|
tmux.send_keys query
tmux.until { |lines| assert lines.any_include?("> #{query}".strip) }
@@ -1228,7 +1228,7 @@ class TestCore < TestInteractive
end
def test_freeze_right_exceed_range_with_freeze_left
tmux.send_keys %(seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 100000 --ellipsis XX), :Enter
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 100000 --ellipsis XX], :Enter
tmux.until do |lines|
assert_match(/^> 1 2 3XX.*9998 9999 10000$/, lines[-3])
assert_equal(1, lines[-3].scan('XX').size)
@@ -1241,7 +1241,7 @@ class TestCore < TestInteractive
tmux.send_keys(*Array.new(6) { :a })
tmux.until do |lines|
assert_match(/> 777g+$/, lines[-3])
assert_equal(1, lines.count { |l| l.end_with?('g') })
assert_equal 1, lines.count { |l| l.end_with?('g') }
end
end
@@ -1588,16 +1588,14 @@ class TestCore < TestInteractive
end
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --pointer x --query 555 --bind t:track,T:up+track", :Enter
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, 'x 555'
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, 'x 55'
assert_includes lines, '> 55'
end
tmux.send_keys :t
@@ -1607,8 +1605,7 @@ class TestCore < TestInteractive
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, 'x 55'
assert_includes lines, '> 5'
assert_includes lines, '> 55'
end
# Automatically disabled when the tracking item is no longer visible
@@ -1620,33 +1617,16 @@ class TestCore < TestInteractive
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, 'x 52'
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+t'
end
# Automatically disabled when the focus has moved
tmux.send_keys :Up
tmux.until do |lines|
assert_includes lines, 'x 53'
refute_includes lines[-2], '+t'
end
# Should work even when combined with a focus moving actions
tmux.send_keys 'T'
tmux.until do |lines|
assert_includes lines, 'x 54'
assert_includes lines[-2], '+t'
end
tmux.send_keys 'T'
tmux.until do |lines|
assert_includes lines, 'x 55'
assert_includes lines[-2], '+t'
end
end
def test_one_and_zero
@@ -1755,7 +1735,7 @@ class TestCore < TestInteractive
end
end
tmux.send_keys %({ echo foo; seq 100; } | #{FZF} --header-lines 1 --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
expected = {
FZF_DIRECTION: 'down',
FZF_TOTAL_COUNT: '100',
@@ -2175,87 +2155,4 @@ class TestCore < TestInteractive
assert_equal 1, it.match_count
end
end
def test_change_header_lines
tmux.send_keys %(seq 10 | #{FZF} --header-lines 3 --bind 'space:change-header-lines(5),enter:transform-header-lines(echo 1)'), :Enter
tmux.until do |lines|
assert_equal 7, lines.item_count
assert lines.any_include?('> 4')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 5, lines.item_count
assert lines.any_include?('> 6')
end
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal 9, lines.item_count
assert lines.any_include?('> 6')
end
end
def test_change_header_lines_to_zero
tmux.send_keys %(seq 5 | #{FZF} --header-lines 3 --bind 'space:bg-transform-header-lines(echo 0)'), :Enter
tmux.until do |lines|
assert_equal 2, lines.item_count
assert lines.any_include?('> 4')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 5, lines.item_count
# All items are now in the list, cursor stays on item 4
assert lines.any_include?('> 4')
end
end
def test_change_header_lines_deselect
# Selected items that become part of the header should be deselected
tmux.send_keys %(seq 10 | #{FZF} --multi --header-lines 0 --bind 'space:change-header-lines(3),enter:change-header-lines(1)'), :Enter
tmux.until do |lines|
assert_equal 10, lines.item_count
assert lines.any_include?('> 1')
end
# Select items 1, 2, 3 (these will become header lines)
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal 3, lines.select_count }
# Also select item 4 (this should remain selected)
tmux.send_keys :BTab
tmux.until { |lines| assert_equal 4, lines.select_count }
# Change header-lines to 3: items 1, 2, 3 become headers and should be deselected
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 7, lines.item_count
assert_equal 1, lines.select_count
assert lines.any_include?('> 5')
end
# Change header-lines to 1
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal 9, lines.item_count
assert_equal 1, lines.select_count
assert lines.any_include?('> 5')
end
end
def test_change_header_lines_reverse
tmux.send_keys %(seq 10 | #{FZF} --header-lines 2 --reverse --bind 'space:change-header-lines(4)'), :Enter
tmux.until do |lines|
assert_equal 8, lines.item_count
assert lines.any_include?('> 3')
end
tmux.send_keys :Space
tmux.until do |lines|
assert_equal 6, lines.item_count
assert lines.any_include?('> 5')
end
end
def test_zero_width_characters
tmux.send_keys %(for i in {1..1000}; do string+="a̱$i"; printf '\\e[43m%s\\e[0m\\n' "$string"; done | #{FZF} --ansi --query a500 --ellipsis XX), :Enter
tmux.until do |lines|
assert_equal 981, lines.match_count
assert_match(/^> XX.*a̱500/, lines[-3])
assert(lines.reverse.drop(5).all? { it.match?(/^ XX.*a̱500.*XX/) })
end
end
end
-37
View File
@@ -312,41 +312,4 @@ class TestFilter < TestBase
assert_equal expected, result
end
end
def test_accept_nth
# Single field selection
assert_equal 'three', `echo 'one two three' | #{FZF} -d' ' --with-nth 1 --accept-nth -1 -f one`.chomp
# Multiple field selection
writelines(['ID001:John:Developer', 'ID002:Jane:Manager', 'ID003:Bob:Designer'])
assert_equal 'ID001', `#{FZF} -d: --with-nth 2 --accept-nth 1 -f John < #{tempname}`.chomp
assert_equal 'ID002:Manager', `#{FZF} -d: --with-nth 2 --accept-nth 1,3 -f Jane < #{tempname}`.chomp
# Test with different delimiters
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
end
def test_header_lines_filter
assert_equal %w[4 5 6 7 8 9 10],
`seq 10 | #{FZF} --header-lines 3 -f ""`.lines(chomp: true)
assert_equal %w[5],
`seq 10 | #{FZF} --header-lines 3 -f 5`.lines(chomp: true)
# Header items should not be matched
assert_empty `seq 10 | #{FZF} --header-lines 3 -f "^1$"`.lines(chomp: true)
end
def test_header_lines_filter_with_nth
writelines(%w[a:1 b:2 c:3 d:4 e:5])
assert_equal %w[c:3 d:4 e:5],
`#{FZF} --header-lines 2 -d: --with-nth 2 -f "" < #{tempname}`.lines(chomp: true)
assert_equal %w[d:4],
`#{FZF} --header-lines 2 -d: --with-nth 2 -f 4 < #{tempname}`.lines(chomp: true)
end
def test_header_lines_all_headers
# When all lines are header lines, no results
assert_empty `seq 3 | #{FZF} --header-lines 10 -f ""`.chomp
assert_equal 1, $CHILD_STATUS.exitstatus
end
end
+1 -38
View File
@@ -383,16 +383,6 @@ class TestPreview < TestInteractive
end
end
def test_preview_follow_wrap
tmux.send_keys "seq 1 | #{FZF} --preview 'seq 1000' --preview-window right,2,follow,wrap", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until do |lines|
idx = lines.rindex { it.include?('│ 10 │') }
assert_includes lines[idx + 1], '│ ↳ │'
assert_includes lines[idx + 2], '│ ↳ │'
end
end
def test_close
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -551,7 +541,7 @@ class TestPreview < TestInteractive
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
tmux.until do |lines|
assert_equal ['╭────╮', '│ 10 │', '│ ↳ │', '│ 10 │', '│ ↳ │'], lines.take(5).map(&:strip)
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
end
end
@@ -582,31 +572,4 @@ class TestPreview < TestInteractive
assert_equal 1, lines.match_count
end
end
def test_preview_wrap_sign_between_ansi_fragments
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 10,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 1234567890 │') })
assert_equal(2, lines.count { |line| line.include?('│ ↳ hello │') })
end
end
def test_preview_wrap_sign_between_ansi_fragments_overflow
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 2,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 12 │') })
assert_equal(0, lines.count { |line| line.include?('│ h') })
end
end
def test_preview_wrap_sign_between_ansi_fragments_overflow2
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 1,wrap-word), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 1 │') })
assert_equal(0, lines.count { |line| line.include?('│ h') })
end
end
end
+3 -13
View File
@@ -31,32 +31,22 @@ class TestServer < TestInteractive
end
def test_listen_with_api_key
uri = URI('http://localhost:6266')
post_uri = URI('http://localhost:6266')
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.match_count }
# Incorrect API Key
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
res = Net::HTTP.get_response(uri, headers)
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '401', res.code
assert_equal 'Unauthorized', res.message
assert_equal "invalid api key\n", res.body
end
# Valid API Key
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
res = Net::HTTP.post(uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
assert_equal '200', res.code
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
res = Net::HTTP.get_response(uri, headers)
assert_equal '200', res.code
assert_equal 'yo', JSON.parse(res.body, symbolize_names: true)[:query]
end
end
end
+76 -614
View File
@@ -27,10 +27,6 @@ module TestShell
tmux.prepare
end
def trigger
'**'
end
def test_ctrl_t
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
@@ -138,7 +134,7 @@ module TestShell
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar'
tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
@@ -169,11 +165,7 @@ module CompletionTest
FileUtils.touch(File.expand_path(f))
end
tmux.prepare
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/10', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
end
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys ' !d'
tmux.until { |lines| assert_equal 2, lines.match_count }
@@ -187,11 +179,7 @@ module CompletionTest
# ~USERNAME**<TAB>
user = `whoami`.chomp
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys "cat ~#{user}", 'C-t'
else
tmux.send_keys "cat ~#{user}#{trigger}", :Tab
end
tmux.send_keys "cat ~#{user}**", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys "/#{user}"
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
@@ -202,29 +190,14 @@ module CompletionTest
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat ~such', 'C-t'
else
tmux.send_keys "cat ~such#{trigger}", :Tab
end
tmux.send_keys 'cat ~such**', :Tab
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
tmux.send_keys :Enter
tmux.until(true) do |lines|
if shell == :fish
# Fish's string escape quotes filenames with ~ to prevent tilde expansion
assert_equal 'cat no\\~such\\~user', lines[-1]
else
assert_equal 'cat no~such~user', lines[-1]
end
end
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat /tmp/fzf\\ test/', 'C-t'
else
tmux.send_keys "cat /tmp/fzf\\ test/#{trigger}", :Tab
end
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'foobar$'
tmux.until do |lines|
@@ -237,11 +210,7 @@ module CompletionTest
# Should include hidden files
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/hidden', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
end
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until(true) do |lines|
assert_equal 100, lines.match_count
assert lines.any_include?('/tmp/fzf-test/.hidden-')
@@ -254,89 +223,70 @@ module CompletionTest
end
def test_file_completion_root
if shell == :fish
tmux.send_keys 'ls /', 'C-t'
else
tmux.send_keys "ls /#{trigger}", :Tab
end
tmux.send_keys 'ls /**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Enter
end
def test_dir_completion
FileUtils.mkdir_p('/tmp/fzf-test-dir')
(1..100).each do |idx|
FileUtils.mkdir_p("/tmp/fzf-test-dir/d#{idx}")
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
end
FileUtils.touch('/tmp/fzf-test-dir/d55/xxx')
FileUtils.touch('/tmp/fzf-test/d55/xxx')
tmux.prepare
if shell == :fish
tmux.send_keys 'cd /tmp/fzf-test-dir/', 'C-t'
else
tmux.send_keys "cd /tmp/fzf-test-dir/#{trigger}", :Tab
end
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
# Tab selects items in C-t's --multi mode, so skip for fish
tmux.send_keys :Tab, :Tab unless shell == :fish # Tab does not work here
tmux.send_keys '55/$'
tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55/$'
assert_includes lines, '> /tmp/fzf-test-dir/d55/'
assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55/'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
# C-t appends a trailing space after the result
tmux.send_keys :BSpace if shell == :fish
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.send_keys :xx
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
# Should not match regular files (bash-only)
if instance_of?(TestBash)
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
end
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55', lines[-1] }
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dir')
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
end
def test_process_completion
skip('fish background job format differs') if shell == :fish
begin
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys "kill #{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
end
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys 'kill **', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
end
end
end
def test_custom_completion
skip('fish does not use _fzf_compgen_path; path completion is via ctrl-t') if shell == :fish
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys "ls /tmp/#{trigger}", :Tab
tmux.send_keys 'ls /tmp/**', :Tab
tmux.until { |lines| assert_equal 11, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert_equal 3, lines.select_count }
@@ -345,12 +295,11 @@ module CompletionTest
end
def test_unset_completion
skip('fish has native completion for set and unset variables') if shell == :fish
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
# Using tmux
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
@@ -359,57 +308,34 @@ module CompletionTest
# FZF_TMUX=1
new_shell
tmux.focus
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.send_keys 'unset FZFFOOBR**', :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
end
def test_completion_in_command_sequence
if shell == :fish
FileUtils.mkdir_p('/tmp/fzf-test-seq')
FileUtils.touch('/tmp/fzf-test-seq/fzffoobar')
tmux.prepare
# Fish uses Shift-Tab for fzf completion (no trigger system)
command = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobr'
expected = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobar'
tmux.send_keys command, :BTab
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal expected, lines[-1] }
else
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
expected = 'echo foo; QUX=THUD unset FZFFOOBAR'
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal expected, lines[-1] }
end
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-seq') if shell == :fish
end
def test_file_completion_unicode
FileUtils.mkdir_p('/tmp/fzf-test')
# Shell-agnostic file creation
File.write('/tmp/fzf-test/fzf-unicode 테스트1', "test3\n")
File.write('/tmp/fzf-test/fzf-unicode 테스트2', "test4\n")
tmux.send_keys 'cd /tmp/fzf-test', :Enter
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
tmux.prepare
if shell == :fish
tmux.send_keys 'cat fzf-unicode', 'C-t'
else
tmux.send_keys "cat fzf-unicode#{trigger}", :Tab
end
tmux.send_keys 'cat fzf-unicode**', :Tab
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
@@ -432,41 +358,36 @@ module CompletionTest
end
def test_custom_completion_api
skip('bash-specific _comprun/declare syntax') if shell == :fish
begin
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys "#{command} b#{trigger}", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
end
ensure
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
tmux.send_keys "#{command} b**", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
end
ensure
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
end
def test_ssh_completion
skip('fish uses native ssh completion') if shell == :fish
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
tmux.send_keys "ssh jg@localhost#{trigger}", :Tab
tmux.send_keys 'ssh jg@localhost**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
tmux.send_keys " -i /tmp/fzf-test-ssh#{trigger}", :Tab
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 5
assert_equal 0, lines.select_count
@@ -478,344 +399,11 @@ module CompletionTest
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
tmux.send_keys "localhost#{trigger}", :Tab
tmux.send_keys 'localhost**', :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
end
def test_option_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long')
end
def test_option_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command -- --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- --opt=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long-ddash')
end
def test_option_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -o=SECURI#{trigger}", :Tab
end
case shell
when :bash, :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -o=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short')
end
def test_option_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -- -o=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURITY.md'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- -o=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short-ddash')
end
def test_option_no_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --optSECURI', 'C-t'
else
tmux.send_keys "some-command --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long')
end
def test_option_no_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --optSECURI', 'C-t'
else
tmux.send_keys "some-command -- --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long-ddash')
end
def test_option_no_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -oSECURI', 'C-t'
else
tmux.send_keys "some-command -oSECURI#{trigger}", :Tab
end
case shell
when :bash, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -oSECURITY.md', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short')
end
def test_option_no_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -oSECURI', 'C-t'
else
tmux.send_keys "some-command -- -oSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short-ddash')
end
def test_filename_with_newline
FileUtils.mkdir_p('/tmp/fzf-test-newline')
FileUtils.touch("/tmp/fzf-test-newline/xyz\nwith\nnewlines")
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-newline', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'cat xyz', 'C-t'
else
tmux.send_keys "cat xyz#{trigger}", :Tab
end
case shell
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# fish escapes newlines in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz\\nwith\\nnewlines', lines[-1] }
when :bash, :zsh
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# bash and zsh replace newlines with spaces in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz with newlines', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-newline')
end
def test_path_with_special_chars
FileUtils.mkdir_p('/tmp/fzf-test-[special]')
FileUtils.touch('/tmp/fzf-test-[special]/xyz123')
tmux.prepare
if shell == :fish
tmux.send_keys 'ls /tmp/fzf-test-\[special\]/xyz', 'C-t'
else
tmux.send_keys "ls /tmp/fzf-test-\\[special\\]/xyz#{trigger}", :Tab
end
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls /tmp/fzf-test-\\[special\\]/xyz123', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-[special]')
end
def test_query_with_dollar_anchor
FileUtils.mkdir_p('/tmp/fzf-test-dollar-anchor')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/file.txt')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/filetxt.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-dollar-anchor', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls txt$', 'C-t'
else
tmux.send_keys "ls txt$#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> txt$'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls file.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dollar-anchor')
end
def test_single_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-single-flag')
FileUtils.touch('/tmp/fzf-test-single-flag/-testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-single-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls -', 'C-t'
else
tmux.send_keys "ls -#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> -'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls -testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-single-flag')
end
def test_double_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-double-flag')
FileUtils.touch('/tmp/fzf-test-double-flag/--testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-double-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls --', 'C-t'
else
tmux.send_keys "ls --#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> --'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls --testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-double-flag')
end
end
class TestBash < TestBase
@@ -836,7 +424,7 @@ class TestBash < TestBase
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
tmux.paste '_completion_loader() { complete -o default fake; }'
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'C-c'
@@ -845,7 +433,7 @@ class TestBash < TestBase
tmux.send_keys :Tab, 'C-u'
tmux.prepare
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.send_keys 'fake /tmp/foo**', :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
end
end
@@ -867,142 +455,24 @@ class TestZsh < TestBase
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
['unset', '\unset', "'unset'"].each do |command|
tmux.prepare
tmux.send_keys "#{command} FZFFOOBR#{trigger}", :Tab
tmux.send_keys "#{command} FZFFOOBR**", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
tmux.send_keys 'C-c'
end
end
# Helper function to run test with Perl and again with Awk
def self.test_perl_and_awk(name, &block)
define_method(:"test_#{name}") do
instance_eval(&block)
end
define_method(:"test_#{name}_awk") do
tmux.send_keys "unset 'commands[perl]'", :Enter
tmux.prepare
# Verify perl is actually unset (0 = not found)
tmux.send_keys 'echo ${+commands[perl]}', :Enter
tmux.until { |lines| assert_equal '0', lines[-1] }
tmux.prepare
instance_eval(&block)
end
end
def prepare_ctrl_r_test
tmux.send_keys ':', :Enter
tmux.send_keys 'echo match-collision', :Enter
tmux.prepare
tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
tmux.prepare
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'echo "trailing_space "', :Enter
tmux.prepare
tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
end
test_perl_and_awk 'ctrl_r_accept_or_print_query' do
set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '1 foobar'
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
end
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
# Leading number in multi-line history content is not confused with index
prepare_ctrl_r_test
tmux.send_keys "'line 1"
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
end
end
test_perl_and_awk 'ctrl_r_multi_selection' do
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
end
end
test_perl_and_awk 'ctrl_r_no_multi_selection' do
set_var('FZF_CTRL_R_OPTS', '--no-multi')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| refute_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
end
end
# NOTE: 'Perl/$history' won't see foreign cmds immediately, unlike 'awk/fc'.
# Perl passes only because another cmd runs between mocking and triggering C-r
# https://github.com/junegunn/fzf/issues/4061
# https://zsh.org/mla/users/2024/msg00692.html
test_perl_and_awk 'ctrl_r_foreign_commands' do
histfile = "#{tempname}-foreign-hist"
tmux.send_keys "HISTFILE=#{histfile}", :Enter
tmux.prepare
# SHARE_HISTORY picks up foreign commands; marked with * in fc
tmux.send_keys 'setopt SHARE_HISTORY', :Enter
tmux.prepare
tmux.send_keys 'fzf_cmd_local', :Enter
tmux.prepare
# Mock foreign command (for testing only; don't edit your HISTFILE this way)
tmux.send_keys "echo ': 0:0;fzf_cmd_foreign' >> $HISTFILE", :Enter
tmux.prepare
# Verify fc shows foreign command with asterisk
tmux.send_keys 'fc -rl -1', :Enter
tmux.until { |lines| assert(lines.any? { |l| l.match?(/^\s*\d+\* fzf_cmd_foreign/) }) }
tmux.prepare
# Test ctrl-r correctly extracts the foreign command
tmux.send_keys 'C-r'
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '^fzf_cmd_'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal %w[fzf_cmd_foreign fzf_cmd_local], lines[-2..]
end
ensure
FileUtils.rm_f(histfile)
end
end
class TestFish < TestBase
include TestShell
include CompletionTest
def shell
:fish
end
def trigger
'++'
end
def new_shell
tmux.send_keys 'env FZF_TMUX=1 XDG_CONFIG_HOME=/tmp/fzf-fish fish', :Enter
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines }
end
@@ -1020,23 +490,15 @@ class TestFish < TestBase
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
offset = -6
block = <<~BLOCK
echo "foo
bar"
echo "bar
foo"
BLOCK
if shell == :fish
offset = -4
block = <<~FISH
echo "foo␊bar"
echo "bar␊foo"
FISH
end
tmux.until do |lines|
block.lines.each_with_index do |line, idx|
assert_includes lines[idx + offset], line.chomp
assert_includes lines[-6 + idx], line.chomp
end
end
tmux.send_keys :BTab, :BTab
-6
View File
@@ -33,9 +33,6 @@ for opt in "$@"; do
esac
done
cd "$(dirname "${BASH_SOURCE[0]}")"
fzf_base=$(pwd)
ask() {
while true; do
read -p "$1 ([y]/n) " -r
@@ -97,15 +94,12 @@ done
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ -f "$bind_file" ]; then
remove_line "$bind_file" "fzf_key_bindings"
remove_line "$bind_file" "fzf_completion_setup"
remove_line "$bind_file" "fzf --fish | source"
fi
if [ -d "${fish_dir}/functions" ]; then
remove "${fish_dir}/functions/fzf.fish"
remove "${fish_dir}/functions/fzf_key_bindings.fish"
remove_line "$bind_file" "source \"${fzf_base}/shell/completion.fish\""
remove_line "$bind_file" "source \"${fzf_base}/shell/key-bindings.fish\""
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
rmdir "${fish_dir}/functions"