mirror of
https://github.com/junegunn/fzf
synced 2026-08-07 19:31:41 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eacef5ea6e | |||
| 96eb68ce63 | |||
| 50be8bc78e | |||
| b4e585779a | |||
| 97ac7794cf | |||
| 4866c34361 | |||
| 3cfee281b4 | |||
| 5887edc6ba | |||
| 3e751c4e87 | |||
| 8452c78cc8 | |||
| 2db14b4308 | |||
| 90c4269d4e | |||
| 6087055305 | |||
| 2f9df91171 | |||
| 12e24d368c | |||
| 55193ee4dc | |||
| ff6a3bbee0 | |||
| dce248ac6d | |||
| 0ff13dcfbe | |||
| 4d6a7757b8 | |||
| b9804f5873 | |||
| 98a3b1fff8 | |||
| 6df5ca17e8 | |||
| 09ca45f7db | |||
| 09fe3a4180 |
@@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
@@ -44,5 +44,10 @@ jobs:
|
|||||||
- name: Unit test
|
- name: Unit test
|
||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
|
- name: Fuzz test
|
||||||
|
run: |
|
||||||
|
go test ./src/algo/ -fuzz=FuzzIndexByteTwo -fuzztime=5s
|
||||||
|
go test ./src/algo/ -fuzz=FuzzLastIndexByteTwo -fuzztime=5s
|
||||||
|
|
||||||
- name: Integration test
|
- name: Integration test
|
||||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||||
|
|||||||
@@ -1,6 +1,41 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.70.0
|
||||||
|
------
|
||||||
|
- Added `change-with-nth` action for dynamically changing the `--with-nth` option.
|
||||||
|
- Requires `--with-nth` to be set initially.
|
||||||
|
- Multiple options separated by `|` can be given to cycle through.
|
||||||
|
```sh
|
||||||
|
echo -e "a b c\nd e f\ng h i" | fzf --with-nth .. \
|
||||||
|
--bind 'space:change-with-nth(1|2|3|1,3|2,3|)'
|
||||||
|
```
|
||||||
|
- Added `change-header-lines` action for dynamically changing the `--header-lines` option
|
||||||
|
- Performance improvements (1.3x to 1.9x faster filtering depending on query)
|
||||||
|
```
|
||||||
|
=== query: 'l' ===
|
||||||
|
[all] baseline: 168.87ms current: 95.21ms (1.77x) matches: 5069891 (94.78%)
|
||||||
|
[1T] baseline: 1652.22ms current: 841.40ms (1.96x) matches: 5069891 (94.78%)
|
||||||
|
|
||||||
|
=== query: 'lin' ===
|
||||||
|
[all] baseline: 343.27ms current: 252.59ms (1.36x) matches: 3516507 (65.74%)
|
||||||
|
[1T] baseline: 3199.89ms current: 2230.64ms (1.43x) matches: 3516507 (65.74%)
|
||||||
|
|
||||||
|
=== query: 'linux' ===
|
||||||
|
[all] baseline: 85.47ms current: 63.72ms (1.34x) matches: 307229 (5.74%)
|
||||||
|
[1T] baseline: 774.64ms current: 589.32ms (1.31x) matches: 307229 (5.74%)
|
||||||
|
|
||||||
|
=== query: 'linuxlinux' ===
|
||||||
|
[all] baseline: 55.13ms current: 35.67ms (1.55x) matches: 12230 (0.23%)
|
||||||
|
[1T] baseline: 461.99ms current: 332.38ms (1.39x) matches: 12230 (0.23%)
|
||||||
|
|
||||||
|
=== query: 'linuxlinuxlinux' ===
|
||||||
|
[all] baseline: 51.77ms current: 32.53ms (1.59x) matches: 865 (0.02%)
|
||||||
|
[1T] baseline: 409.99ms current: 296.33ms (1.38x) matches: 865 (0.02%)
|
||||||
|
```
|
||||||
|
- Fixed `nth` attribute merge order to respect precedence hierarchy (#4697)
|
||||||
|
- bash: Replaced `printf` with builtin `printf` to bypass local indirections (#4684) (@DarrenBishop)
|
||||||
|
|
||||||
0.68.0
|
0.68.0
|
||||||
------
|
------
|
||||||
- Implemented word wrapping in the list section
|
- Implemented word wrapping in the list section
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.68.0
|
version=0.70.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
$version="0.68.0"
|
$version="0.70.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.68"
|
var version = "0.70"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//go:embed shell/key-bindings.bash
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf\-tmux 1 "Feb 2026" "fzf 0.68.0" "fzf\-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "Mar 2026" "fzf 0.70.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf\-tmux - open fzf in tmux split pane
|
fzf\-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
+27
-1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Feb 2026" "fzf 0.68.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Mar 2026" "fzf 0.70.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -134,6 +134,14 @@ e.g.
|
|||||||
# Use template to rearrange fields
|
# Use template to rearrange fields
|
||||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
||||||
.RE
|
.RE
|
||||||
|
.RS
|
||||||
|
|
||||||
|
\fBchange\-with\-nth\fR action is only available when \fB\-\-with\-nth\fR is set.
|
||||||
|
When \fB\-\-with\-nth\fR is used, fzf retains the original input lines in memory
|
||||||
|
so they can be re\-transformed on the fly (e.g. \fB\-\-with\-nth ..\fR to keep
|
||||||
|
the original presentation). This increases memory usage, so only use
|
||||||
|
\fB\-\-with\-nth\fR when you actually need field transformation.
|
||||||
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||||
Define which fields to print on accept. The last delimiter is stripped from the
|
Define which fields to print on accept. The last delimiter is stripped from the
|
||||||
@@ -1229,6 +1237,18 @@ Here is an example script that uses a Unix socket instead of a TCP port.
|
|||||||
curl --unix-socket /tmp/fzf.sock http -d up
|
curl --unix-socket /tmp/fzf.sock http -d up
|
||||||
\fR
|
\fR
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-threads=" "N"
|
||||||
|
Number of matcher threads to use. The default value is
|
||||||
|
\fBmin(8 * NUM_CPU, 32)\fR.
|
||||||
|
.TP
|
||||||
|
.BI "\-\-bench=" "DURATION"
|
||||||
|
Repeatedly run \fB\-\-filter\fR for the given duration and print timing
|
||||||
|
statistics. Must be used with \fB\-\-filter\fR.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
\fBcat /usr/share/dict/words | fzf \-\-filter abc \-\-bench 10s\fR
|
||||||
|
|
||||||
.SS DIRECTORY TRAVERSAL
|
.SS DIRECTORY TRAVERSAL
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-walker=[file][,dir][,follow][,hidden]"
|
.B "\-\-walker=[file][,dir][,follow][,hidden]"
|
||||||
@@ -1398,6 +1418,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_NTH " Current \-\-nth option"
|
.BR FZF_NTH " Current \-\-nth option"
|
||||||
.br
|
.br
|
||||||
|
.BR FZF_WITH_NTH " Current \-\-with\-nth option"
|
||||||
|
.br
|
||||||
.BR FZF_PROMPT " Prompt string"
|
.BR FZF_PROMPT " Prompt string"
|
||||||
.br
|
.br
|
||||||
.BR FZF_GHOST " Ghost string"
|
.BR FZF_GHOST " Ghost string"
|
||||||
@@ -1881,12 +1903,14 @@ 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\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
||||||
\fBchange\-ghost(...)\fR (change ghost text 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(...)\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\-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\-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)
|
\fBchange\-list\-label(...)\fR (change \fB\-\-list\-label\fR to the given string)
|
||||||
\fBchange\-multi\fR (enable multi-select mode with no limit)
|
\fBchange\-multi\fR (enable multi-select mode with no limit)
|
||||||
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
|
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
|
||||||
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
|
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
|
||||||
|
\fBchange\-with\-nth(...)\fR (change \fB\-\-with\-nth\fR option; rotate through the multiple options separated by '|')
|
||||||
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
|
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
|
||||||
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option)
|
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option)
|
||||||
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
|
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
|
||||||
@@ -1987,10 +2011,12 @@ 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\-border\-label(...)\fR (transform border label using an external command)
|
||||||
\fBtransform\-ghost(...)\fR (transform ghost text 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(...)\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\-header\-label(...)\fR (transform header label using an external command)
|
||||||
\fBtransform\-input\-label(...)\fR (transform input 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)
|
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
||||||
\fBtransform\-nth(...)\fR (transform nth using an external command)
|
\fBtransform\-nth(...)\fR (transform nth using an external command)
|
||||||
|
\fBtransform\-with\-nth(...)\fR (transform with-nth using an external command)
|
||||||
\fBtransform\-pointer(...)\fR (transform pointer using an external command)
|
\fBtransform\-pointer(...)\fR (transform pointer using an external command)
|
||||||
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
|
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
|
||||||
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
|
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
|
||||||
|
|||||||
+10
-10
@@ -38,9 +38,9 @@ if [[ $- =~ i ]]; then
|
|||||||
# the changes. See code comments in "common.sh" for the implementation details.
|
# the changes. See code comments in "common.sh" for the implementation details.
|
||||||
|
|
||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_exec_awk() {
|
__fzf_exec_awk() {
|
||||||
@@ -81,7 +81,7 @@ __fzf_orig_completion() {
|
|||||||
f="${BASH_REMATCH[2]}"
|
f="${BASH_REMATCH[2]}"
|
||||||
cmd="${BASH_REMATCH[3]}"
|
cmd="${BASH_REMATCH[3]}"
|
||||||
[[ $f == _fzf_* ]] && continue
|
[[ $f == _fzf_* ]] && continue
|
||||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
builtin printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||||
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||||
fi
|
fi
|
||||||
@@ -111,7 +111,7 @@ __fzf_orig_completion_instantiate() {
|
|||||||
orig="${!orig_var-}"
|
orig="${!orig_var-}"
|
||||||
orig="${orig%#*}"
|
orig="${orig%#*}"
|
||||||
[[ $orig == *' %s '* ]] || return 1
|
[[ $orig == *' %s '* ]] || return 1
|
||||||
printf -v REPLY "$orig" "$func"
|
builtin printf -v REPLY "$orig" "$func"
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_opts_completion() {
|
_fzf_opts_completion() {
|
||||||
@@ -376,7 +376,7 @@ __fzf_generic_path_completion() {
|
|||||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||||
fi
|
fi
|
||||||
if declare -F "$1" > /dev/null; then
|
if declare -F "$1" > /dev/null; then
|
||||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
eval "$1 $(builtin printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||||
else
|
else
|
||||||
if [[ $1 =~ dir ]]; then
|
if [[ $1 =~ dir ]]; then
|
||||||
walker=dir,follow
|
walker=dir,follow
|
||||||
@@ -385,7 +385,7 @@ __fzf_generic_path_completion() {
|
|||||||
fi
|
fi
|
||||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||||
fi | while read -r item; do
|
fi | while read -r item; do
|
||||||
printf "%q " "${item%$3}$3"
|
builtin printf "%q " "${item%$3}$3"
|
||||||
done
|
done
|
||||||
)
|
)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
@@ -395,9 +395,9 @@ __fzf_generic_path_completion() {
|
|||||||
else
|
else
|
||||||
COMPREPLY=("$cur")
|
COMPREPLY=("$cur")
|
||||||
fi
|
fi
|
||||||
# To redraw line after fzf closes (printf '\e[5n')
|
# To redraw line after fzf closes (builtin printf '\e[5n')
|
||||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
printf '\e[5n'
|
builtin printf '\e[5n'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
dir=$(command dirname "$dir")
|
dir=$(command dirname "$dir")
|
||||||
@@ -455,7 +455,7 @@ _fzf_complete() {
|
|||||||
COMPREPLY=("$cur")
|
COMPREPLY=("$cur")
|
||||||
fi
|
fi
|
||||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
printf '\e[5n'
|
builtin printf '\e[5n'
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||||
@@ -527,7 +527,7 @@ _fzf_proc_completion_post() {
|
|||||||
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
|
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
|
||||||
# local COMPREPLY=()
|
# local COMPREPLY=()
|
||||||
# _known_hosts_real ''
|
# _known_hosts_real ''
|
||||||
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
# builtin printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
||||||
# }
|
# }
|
||||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||||
__fzf_list_hosts() {
|
__fzf_list_hosts() {
|
||||||
|
|||||||
+158
-152
@@ -30,161 +30,167 @@ func _() {
|
|||||||
_ = x[actChangeBorderLabel-19]
|
_ = x[actChangeBorderLabel-19]
|
||||||
_ = x[actChangeGhost-20]
|
_ = x[actChangeGhost-20]
|
||||||
_ = x[actChangeHeader-21]
|
_ = x[actChangeHeader-21]
|
||||||
_ = x[actChangeFooter-22]
|
_ = x[actChangeHeaderLines-22]
|
||||||
_ = x[actChangeHeaderLabel-23]
|
_ = x[actChangeFooter-23]
|
||||||
_ = x[actChangeFooterLabel-24]
|
_ = x[actChangeHeaderLabel-24]
|
||||||
_ = x[actChangeInputLabel-25]
|
_ = x[actChangeFooterLabel-25]
|
||||||
_ = x[actChangeListLabel-26]
|
_ = x[actChangeInputLabel-26]
|
||||||
_ = x[actChangeMulti-27]
|
_ = x[actChangeListLabel-27]
|
||||||
_ = x[actChangeNth-28]
|
_ = x[actChangeMulti-28]
|
||||||
_ = x[actChangePointer-29]
|
_ = x[actChangeNth-29]
|
||||||
_ = x[actChangePreview-30]
|
_ = x[actChangeWithNth-30]
|
||||||
_ = x[actChangePreviewLabel-31]
|
_ = x[actChangePointer-31]
|
||||||
_ = x[actChangePreviewWindow-32]
|
_ = x[actChangePreview-32]
|
||||||
_ = x[actChangePrompt-33]
|
_ = x[actChangePreviewLabel-33]
|
||||||
_ = x[actChangeQuery-34]
|
_ = x[actChangePreviewWindow-34]
|
||||||
_ = x[actClearScreen-35]
|
_ = x[actChangePrompt-35]
|
||||||
_ = x[actClearQuery-36]
|
_ = x[actChangeQuery-36]
|
||||||
_ = x[actClearSelection-37]
|
_ = x[actClearScreen-37]
|
||||||
_ = x[actClose-38]
|
_ = x[actClearQuery-38]
|
||||||
_ = x[actDeleteChar-39]
|
_ = x[actClearSelection-39]
|
||||||
_ = x[actDeleteCharEof-40]
|
_ = x[actClose-40]
|
||||||
_ = x[actEndOfLine-41]
|
_ = x[actDeleteChar-41]
|
||||||
_ = x[actFatal-42]
|
_ = x[actDeleteCharEof-42]
|
||||||
_ = x[actForwardChar-43]
|
_ = x[actEndOfLine-43]
|
||||||
_ = x[actForwardWord-44]
|
_ = x[actFatal-44]
|
||||||
_ = x[actForwardSubWord-45]
|
_ = x[actForwardChar-45]
|
||||||
_ = x[actKillLine-46]
|
_ = x[actForwardWord-46]
|
||||||
_ = x[actKillWord-47]
|
_ = x[actForwardSubWord-47]
|
||||||
_ = x[actKillSubWord-48]
|
_ = x[actKillLine-48]
|
||||||
_ = x[actUnixLineDiscard-49]
|
_ = x[actKillWord-49]
|
||||||
_ = x[actUnixWordRubout-50]
|
_ = x[actKillSubWord-50]
|
||||||
_ = x[actYank-51]
|
_ = x[actUnixLineDiscard-51]
|
||||||
_ = x[actBackwardKillWord-52]
|
_ = x[actUnixWordRubout-52]
|
||||||
_ = x[actBackwardKillSubWord-53]
|
_ = x[actYank-53]
|
||||||
_ = x[actSelectAll-54]
|
_ = x[actBackwardKillWord-54]
|
||||||
_ = x[actDeselectAll-55]
|
_ = x[actBackwardKillSubWord-55]
|
||||||
_ = x[actToggle-56]
|
_ = x[actSelectAll-56]
|
||||||
_ = x[actToggleSearch-57]
|
_ = x[actDeselectAll-57]
|
||||||
_ = x[actToggleAll-58]
|
_ = x[actToggle-58]
|
||||||
_ = x[actToggleDown-59]
|
_ = x[actToggleSearch-59]
|
||||||
_ = x[actToggleUp-60]
|
_ = x[actToggleAll-60]
|
||||||
_ = x[actToggleIn-61]
|
_ = x[actToggleDown-61]
|
||||||
_ = x[actToggleOut-62]
|
_ = x[actToggleUp-62]
|
||||||
_ = x[actToggleTrack-63]
|
_ = x[actToggleIn-63]
|
||||||
_ = x[actToggleTrackCurrent-64]
|
_ = x[actToggleOut-64]
|
||||||
_ = x[actToggleHeader-65]
|
_ = x[actToggleTrack-65]
|
||||||
_ = x[actToggleWrap-66]
|
_ = x[actToggleTrackCurrent-66]
|
||||||
_ = x[actToggleWrapWord-67]
|
_ = x[actToggleHeader-67]
|
||||||
_ = x[actToggleMultiLine-68]
|
_ = x[actToggleWrap-68]
|
||||||
_ = x[actToggleHscroll-69]
|
_ = x[actToggleWrapWord-69]
|
||||||
_ = x[actToggleRaw-70]
|
_ = x[actToggleMultiLine-70]
|
||||||
_ = x[actEnableRaw-71]
|
_ = x[actToggleHscroll-71]
|
||||||
_ = x[actDisableRaw-72]
|
_ = x[actToggleRaw-72]
|
||||||
_ = x[actTrackCurrent-73]
|
_ = x[actEnableRaw-73]
|
||||||
_ = x[actToggleInput-74]
|
_ = x[actDisableRaw-74]
|
||||||
_ = x[actHideInput-75]
|
_ = x[actTrackCurrent-75]
|
||||||
_ = x[actShowInput-76]
|
_ = x[actToggleInput-76]
|
||||||
_ = x[actUntrackCurrent-77]
|
_ = x[actHideInput-77]
|
||||||
_ = x[actDown-78]
|
_ = x[actShowInput-78]
|
||||||
_ = x[actDownMatch-79]
|
_ = x[actUntrackCurrent-79]
|
||||||
_ = x[actUp-80]
|
_ = x[actDown-80]
|
||||||
_ = x[actUpMatch-81]
|
_ = x[actDownMatch-81]
|
||||||
_ = x[actPageUp-82]
|
_ = x[actUp-82]
|
||||||
_ = x[actPageDown-83]
|
_ = x[actUpMatch-83]
|
||||||
_ = x[actPosition-84]
|
_ = x[actPageUp-84]
|
||||||
_ = x[actHalfPageUp-85]
|
_ = x[actPageDown-85]
|
||||||
_ = x[actHalfPageDown-86]
|
_ = x[actPosition-86]
|
||||||
_ = x[actOffsetUp-87]
|
_ = x[actHalfPageUp-87]
|
||||||
_ = x[actOffsetDown-88]
|
_ = x[actHalfPageDown-88]
|
||||||
_ = x[actOffsetMiddle-89]
|
_ = x[actOffsetUp-89]
|
||||||
_ = x[actJump-90]
|
_ = x[actOffsetDown-90]
|
||||||
_ = x[actJumpAccept-91]
|
_ = x[actOffsetMiddle-91]
|
||||||
_ = x[actPrintQuery-92]
|
_ = x[actJump-92]
|
||||||
_ = x[actRefreshPreview-93]
|
_ = x[actJumpAccept-93]
|
||||||
_ = x[actReplaceQuery-94]
|
_ = x[actPrintQuery-94]
|
||||||
_ = x[actToggleSort-95]
|
_ = x[actRefreshPreview-95]
|
||||||
_ = x[actShowPreview-96]
|
_ = x[actReplaceQuery-96]
|
||||||
_ = x[actHidePreview-97]
|
_ = x[actToggleSort-97]
|
||||||
_ = x[actTogglePreview-98]
|
_ = x[actShowPreview-98]
|
||||||
_ = x[actTogglePreviewWrap-99]
|
_ = x[actHidePreview-99]
|
||||||
_ = x[actTogglePreviewWrapWord-100]
|
_ = x[actTogglePreview-100]
|
||||||
_ = x[actTransform-101]
|
_ = x[actTogglePreviewWrap-101]
|
||||||
_ = x[actTransformBorderLabel-102]
|
_ = x[actTogglePreviewWrapWord-102]
|
||||||
_ = x[actTransformGhost-103]
|
_ = x[actTransform-103]
|
||||||
_ = x[actTransformHeader-104]
|
_ = x[actTransformBorderLabel-104]
|
||||||
_ = x[actTransformFooter-105]
|
_ = x[actTransformGhost-105]
|
||||||
_ = x[actTransformHeaderLabel-106]
|
_ = x[actTransformHeader-106]
|
||||||
_ = x[actTransformFooterLabel-107]
|
_ = x[actTransformHeaderLines-107]
|
||||||
_ = x[actTransformInputLabel-108]
|
_ = x[actTransformFooter-108]
|
||||||
_ = x[actTransformListLabel-109]
|
_ = x[actTransformHeaderLabel-109]
|
||||||
_ = x[actTransformNth-110]
|
_ = x[actTransformFooterLabel-110]
|
||||||
_ = x[actTransformPointer-111]
|
_ = x[actTransformInputLabel-111]
|
||||||
_ = x[actTransformPreviewLabel-112]
|
_ = x[actTransformListLabel-112]
|
||||||
_ = x[actTransformPrompt-113]
|
_ = x[actTransformNth-113]
|
||||||
_ = x[actTransformQuery-114]
|
_ = x[actTransformWithNth-114]
|
||||||
_ = x[actTransformSearch-115]
|
_ = x[actTransformPointer-115]
|
||||||
_ = x[actTrigger-116]
|
_ = x[actTransformPreviewLabel-116]
|
||||||
_ = x[actBgTransform-117]
|
_ = x[actTransformPrompt-117]
|
||||||
_ = x[actBgTransformBorderLabel-118]
|
_ = x[actTransformQuery-118]
|
||||||
_ = x[actBgTransformGhost-119]
|
_ = x[actTransformSearch-119]
|
||||||
_ = x[actBgTransformHeader-120]
|
_ = x[actTrigger-120]
|
||||||
_ = x[actBgTransformFooter-121]
|
_ = x[actBgTransform-121]
|
||||||
_ = x[actBgTransformHeaderLabel-122]
|
_ = x[actBgTransformBorderLabel-122]
|
||||||
_ = x[actBgTransformFooterLabel-123]
|
_ = x[actBgTransformGhost-123]
|
||||||
_ = x[actBgTransformInputLabel-124]
|
_ = x[actBgTransformHeader-124]
|
||||||
_ = x[actBgTransformListLabel-125]
|
_ = x[actBgTransformHeaderLines-125]
|
||||||
_ = x[actBgTransformNth-126]
|
_ = x[actBgTransformFooter-126]
|
||||||
_ = x[actBgTransformPointer-127]
|
_ = x[actBgTransformHeaderLabel-127]
|
||||||
_ = x[actBgTransformPreviewLabel-128]
|
_ = x[actBgTransformFooterLabel-128]
|
||||||
_ = x[actBgTransformPrompt-129]
|
_ = x[actBgTransformInputLabel-129]
|
||||||
_ = x[actBgTransformQuery-130]
|
_ = x[actBgTransformListLabel-130]
|
||||||
_ = x[actBgTransformSearch-131]
|
_ = x[actBgTransformNth-131]
|
||||||
_ = x[actBgCancel-132]
|
_ = x[actBgTransformWithNth-132]
|
||||||
_ = x[actSearch-133]
|
_ = x[actBgTransformPointer-133]
|
||||||
_ = x[actPreview-134]
|
_ = x[actBgTransformPreviewLabel-134]
|
||||||
_ = x[actPreviewTop-135]
|
_ = x[actBgTransformPrompt-135]
|
||||||
_ = x[actPreviewBottom-136]
|
_ = x[actBgTransformQuery-136]
|
||||||
_ = x[actPreviewUp-137]
|
_ = x[actBgTransformSearch-137]
|
||||||
_ = x[actPreviewDown-138]
|
_ = x[actBgCancel-138]
|
||||||
_ = x[actPreviewPageUp-139]
|
_ = x[actSearch-139]
|
||||||
_ = x[actPreviewPageDown-140]
|
_ = x[actPreview-140]
|
||||||
_ = x[actPreviewHalfPageUp-141]
|
_ = x[actPreviewTop-141]
|
||||||
_ = x[actPreviewHalfPageDown-142]
|
_ = x[actPreviewBottom-142]
|
||||||
_ = x[actPrevHistory-143]
|
_ = x[actPreviewUp-143]
|
||||||
_ = x[actPrevSelected-144]
|
_ = x[actPreviewDown-144]
|
||||||
_ = x[actPrint-145]
|
_ = x[actPreviewPageUp-145]
|
||||||
_ = x[actPut-146]
|
_ = x[actPreviewPageDown-146]
|
||||||
_ = x[actNextHistory-147]
|
_ = x[actPreviewHalfPageUp-147]
|
||||||
_ = x[actNextSelected-148]
|
_ = x[actPreviewHalfPageDown-148]
|
||||||
_ = x[actExecute-149]
|
_ = x[actPrevHistory-149]
|
||||||
_ = x[actExecuteSilent-150]
|
_ = x[actPrevSelected-150]
|
||||||
_ = x[actExecuteMulti-151]
|
_ = x[actPrint-151]
|
||||||
_ = x[actSigStop-152]
|
_ = x[actPut-152]
|
||||||
_ = x[actBest-153]
|
_ = x[actNextHistory-153]
|
||||||
_ = x[actFirst-154]
|
_ = x[actNextSelected-154]
|
||||||
_ = x[actLast-155]
|
_ = x[actExecute-155]
|
||||||
_ = x[actReload-156]
|
_ = x[actExecuteSilent-156]
|
||||||
_ = x[actReloadSync-157]
|
_ = x[actExecuteMulti-157]
|
||||||
_ = x[actDisableSearch-158]
|
_ = x[actSigStop-158]
|
||||||
_ = x[actEnableSearch-159]
|
_ = x[actBest-159]
|
||||||
_ = x[actSelect-160]
|
_ = x[actFirst-160]
|
||||||
_ = x[actDeselect-161]
|
_ = x[actLast-161]
|
||||||
_ = x[actUnbind-162]
|
_ = x[actReload-162]
|
||||||
_ = x[actRebind-163]
|
_ = x[actReloadSync-163]
|
||||||
_ = x[actToggleBind-164]
|
_ = x[actDisableSearch-164]
|
||||||
_ = x[actBecome-165]
|
_ = x[actEnableSearch-165]
|
||||||
_ = x[actShowHeader-166]
|
_ = x[actSelect-166]
|
||||||
_ = x[actHideHeader-167]
|
_ = x[actDeselect-167]
|
||||||
_ = x[actBell-168]
|
_ = x[actUnbind-168]
|
||||||
_ = x[actExclude-169]
|
_ = x[actRebind-169]
|
||||||
_ = x[actExcludeMulti-170]
|
_ = x[actToggleBind-170]
|
||||||
_ = x[actAsync-171]
|
_ = x[actBecome-171]
|
||||||
|
_ = x[actShowHeader-172]
|
||||||
|
_ = x[actHideHeader-173]
|
||||||
|
_ = x[actBell-174]
|
||||||
|
_ = x[actExclude-175]
|
||||||
|
_ = x[actExcludeMulti-176]
|
||||||
|
_ = x[actAsync-177]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||||
|
|
||||||
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, 992, 1010, 1026, 1038, 1050, 1063, 1078, 1092, 1104, 1116, 1133, 1140, 1152, 1157, 1167, 1176, 1187, 1198, 1211, 1226, 1237, 1250, 1265, 1272, 1285, 1298, 1315, 1330, 1343, 1357, 1371, 1387, 1407, 1431, 1443, 1466, 1483, 1501, 1519, 1542, 1565, 1587, 1608, 1623, 1642, 1666, 1684, 1701, 1719, 1729, 1743, 1768, 1787, 1807, 1827, 1852, 1877, 1901, 1924, 1941, 1962, 1988, 2008, 2027, 2047, 2058, 2067, 2077, 2090, 2106, 2118, 2132, 2148, 2166, 2186, 2208, 2222, 2237, 2245, 2251, 2265, 2280, 2290, 2306, 2321, 2331, 2338, 2346, 2353, 2362, 2375, 2391, 2406, 2415, 2426, 2435, 2444, 2457, 2466, 2479, 2492, 2499, 2509, 2524, 2532}
|
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, 502, 523, 545, 560, 574, 588, 601, 618, 626, 639, 655, 667, 675, 689, 703, 720, 731, 742, 756, 774, 791, 798, 817, 839, 851, 865, 874, 889, 901, 914, 925, 936, 948, 962, 983, 998, 1011, 1028, 1046, 1062, 1074, 1086, 1099, 1114, 1128, 1140, 1152, 1169, 1176, 1188, 1193, 1203, 1212, 1223, 1234, 1247, 1262, 1273, 1286, 1301, 1308, 1321, 1334, 1351, 1366, 1379, 1393, 1407, 1423, 1443, 1467, 1479, 1502, 1519, 1537, 1560, 1578, 1601, 1624, 1646, 1667, 1682, 1701, 1720, 1744, 1762, 1779, 1797, 1807, 1821, 1846, 1865, 1885, 1910, 1930, 1955, 1980, 2004, 2027, 2044, 2065, 2086, 2112, 2132, 2151, 2171, 2182, 2191, 2201, 2214, 2230, 2242, 2256, 2272, 2290, 2310, 2332, 2346, 2361, 2369, 2375, 2389, 2404, 2414, 2430, 2445, 2455, 2462, 2470, 2477, 2486, 2499, 2515, 2530, 2539, 2550, 2559, 2568, 2581, 2590, 2603, 2616, 2623, 2633, 2648, 2656}
|
||||||
|
|
||||||
func (i actionType) String() string {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# SIMD byte search: `indexByteTwo` / `lastIndexByteTwo`
|
||||||
|
|
||||||
|
## What these functions do
|
||||||
|
|
||||||
|
`indexByteTwo(s []byte, b1, b2 byte) int` — returns the index of the
|
||||||
|
**first** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||||
|
|
||||||
|
`lastIndexByteTwo(s []byte, b1, b2 byte) int` — returns the index of the
|
||||||
|
**last** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||||
|
|
||||||
|
They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead
|
||||||
|
during case-insensitive search. Instead of calling `bytes.IndexByte` twice
|
||||||
|
(once for lowercase, once for uppercase), a single SIMD pass finds both at
|
||||||
|
once.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| ------ | --------- |
|
||||||
|
| `indexbyte2_arm64.go` | Go declarations (`//go:noescape`) for ARM64 |
|
||||||
|
| `indexbyte2_arm64.s` | ARM64 NEON assembly (32-byte aligned blocks, syndrome extraction) |
|
||||||
|
| `indexbyte2_amd64.go` | Go declarations + AVX2 runtime detection for AMD64 |
|
||||||
|
| `indexbyte2_amd64.s` | AMD64 AVX2/SSE2 assembly with CPUID dispatch |
|
||||||
|
| `indexbyte2_other.go` | Pure Go fallback for all other architectures |
|
||||||
|
| `indexbyte2_test.go` | Unit tests, exhaustive tests, fuzz tests, and benchmarks |
|
||||||
|
|
||||||
|
## How the SIMD implementations work
|
||||||
|
|
||||||
|
**ARM64 (NEON):**
|
||||||
|
- Broadcasts both needle bytes into NEON registers (`VMOV`).
|
||||||
|
- Processes 32-byte aligned chunks. For each chunk, compares all bytes
|
||||||
|
against both needles (`VCMEQ`), ORs the results (`VORR`), and builds a
|
||||||
|
64-bit syndrome with 2 bits per byte.
|
||||||
|
- `indexByteTwo` uses `RBIT` + `CLZ` to find the lowest set bit (first match).
|
||||||
|
- `lastIndexByteTwo` scans backward and uses `CLZ` on the raw syndrome to
|
||||||
|
find the highest set bit (last match).
|
||||||
|
- Handles alignment and partial first/last blocks with bit masking.
|
||||||
|
- Adapted from Go's `internal/bytealg/indexbyte_arm64.s`.
|
||||||
|
|
||||||
|
**AMD64 (AVX2 with SSE2 fallback):**
|
||||||
|
- At init time, `cpuHasAVX2()` checks CPUID + XGETBV for AVX2 and OS YMM
|
||||||
|
support. The result is cached in `_useAVX2`.
|
||||||
|
- **AVX2 path** (inputs >= 32 bytes, when available):
|
||||||
|
- Broadcasts both needles via `VPBROADCASTB`.
|
||||||
|
- Processes 32-byte blocks: `VPCMPEQB` against both needles, `VPOR`, then
|
||||||
|
`VPMOVMSKB` to get a 32-bit mask.
|
||||||
|
- 5 instructions per loop iteration (vs 7 for SSE2) at 2x the throughput.
|
||||||
|
- `VZEROUPPER` before every return to avoid SSE/AVX transition penalties.
|
||||||
|
- **SSE2 fallback** (inputs < 32 bytes, or CPUs without AVX2):
|
||||||
|
- Broadcasts via `PUNPCKLBW` + `PSHUFL`.
|
||||||
|
- Processes 16-byte blocks: `PCMPEQB`, `POR`, `PMOVMSKB`.
|
||||||
|
- Small inputs (<16 bytes) are handled with page-boundary-safe loads.
|
||||||
|
- Both paths use `BSFL` (forward) / `BSRL` (reverse) for bit scanning.
|
||||||
|
- Adapted from Go's `internal/bytealg/indexbyte_amd64.s`.
|
||||||
|
|
||||||
|
**Fallback (other platforms):**
|
||||||
|
- `indexByteTwo` uses two `bytes.IndexByte` calls with scope-limiting
|
||||||
|
(search `b1` first, then limit the `b2` search to `s[:i1]`).
|
||||||
|
- `lastIndexByteTwo` uses a simple backward for loop.
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Unit + exhaustive tests
|
||||||
|
go test ./src/algo/ -run 'TestIndexByteTwo|TestLastIndexByteTwo' -v
|
||||||
|
|
||||||
|
# Fuzz tests (run for 10 seconds each)
|
||||||
|
go test ./src/algo/ -run '^$' -fuzz FuzzIndexByteTwo -fuzztime 10s
|
||||||
|
go test ./src/algo/ -run '^$' -fuzz FuzzLastIndexByteTwo -fuzztime 10s
|
||||||
|
|
||||||
|
# Cross-architecture: test amd64 on an arm64 Mac (via Rosetta)
|
||||||
|
GOARCH=amd64 go test ./src/algo/ -run 'TestIndexByteTwo|TestLastIndexByteTwo' -v
|
||||||
|
GOARCH=amd64 go test ./src/algo/ -run '^$' -fuzz FuzzIndexByteTwo -fuzztime 10s
|
||||||
|
GOARCH=amd64 go test ./src/algo/ -run '^$' -fuzz FuzzLastIndexByteTwo -fuzztime 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running micro-benchmarks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All indexByteTwo / lastIndexByteTwo benchmarks
|
||||||
|
go test ./src/algo/ -bench 'IndexByteTwo' -benchmem
|
||||||
|
|
||||||
|
# Specific size
|
||||||
|
go test ./src/algo/ -bench 'IndexByteTwo_1000'
|
||||||
|
```
|
||||||
|
|
||||||
|
Each benchmark compares the SIMD `asm` implementation against reference
|
||||||
|
implementations (`2xIndexByte` using `bytes.IndexByte`, and a simple `loop`).
|
||||||
|
|
||||||
|
## Correctness verification
|
||||||
|
|
||||||
|
The assembly is verified by three layers of testing:
|
||||||
|
|
||||||
|
1. **Table-driven tests** — known inputs with expected outputs.
|
||||||
|
2. **Exhaustive tests** — all lengths 0–256, every match position, no-match
|
||||||
|
cases, and both-bytes-present cases, compared against a simple loop
|
||||||
|
reference.
|
||||||
|
3. **Fuzz tests** — randomized inputs via `testing.F`, compared against the
|
||||||
|
same loop reference.
|
||||||
+16
-20
@@ -321,22 +321,15 @@ type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Cha
|
|||||||
|
|
||||||
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
||||||
byteArray := input.Bytes()[from:]
|
byteArray := input.Bytes()[from:]
|
||||||
idx := bytes.IndexByte(byteArray, b)
|
// For case-insensitive search of a letter, search for both cases in one pass
|
||||||
if idx == 0 {
|
|
||||||
// Can't skip any further
|
|
||||||
return from
|
|
||||||
}
|
|
||||||
// We may need to search for the uppercase letter again. We don't have to
|
|
||||||
// consider normalization as we can be sure that this is an ASCII string.
|
|
||||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||||
if idx > 0 {
|
idx := indexByteTwo(byteArray, b, b-32)
|
||||||
byteArray = byteArray[:idx]
|
if idx < 0 {
|
||||||
}
|
return -1
|
||||||
uidx := bytes.IndexByte(byteArray, b-32)
|
|
||||||
if uidx >= 0 {
|
|
||||||
idx = uidx
|
|
||||||
}
|
}
|
||||||
|
return from + idx
|
||||||
}
|
}
|
||||||
|
idx := bytes.IndexByte(byteArray, b)
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -380,14 +373,17 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the last appearance of the last character of the pattern to limit the search scope
|
// Find the last appearance of the last character of the pattern to limit the search scope
|
||||||
bu := b
|
|
||||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
|
||||||
bu = b - 32
|
|
||||||
}
|
|
||||||
scope := input.Bytes()[lastIdx:]
|
scope := input.Bytes()[lastIdx:]
|
||||||
for offset := len(scope) - 1; offset > 0; offset-- {
|
if len(scope) > 1 {
|
||||||
if scope[offset] == b || scope[offset] == bu {
|
tail := scope[1:]
|
||||||
return firstIdx, lastIdx + offset + 1
|
var end int
|
||||||
|
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||||
|
end = lastIndexByteTwo(tail, b, b-32)
|
||||||
|
} else {
|
||||||
|
end = bytes.LastIndexByte(tail, b)
|
||||||
|
}
|
||||||
|
if end >= 0 {
|
||||||
|
return firstIdx, lastIdx + 1 + end + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return firstIdx, lastIdx + 1
|
return firstIdx, lastIdx + 1
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//go:build amd64
|
||||||
|
|
||||||
|
package algo
|
||||||
|
|
||||||
|
var _useAVX2 bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_useAVX2 = cpuHasAVX2()
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func cpuHasAVX2() bool
|
||||||
|
|
||||||
|
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present. Uses AVX2 when available, SSE2 otherwise.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func indexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
|
||||||
|
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present. Uses AVX2 when available, SSE2 otherwise.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func cpuHasAVX2() bool
|
||||||
|
//
|
||||||
|
// Checks CPUID and XGETBV for AVX2 + OS YMM support.
|
||||||
|
TEXT ·cpuHasAVX2(SB),NOSPLIT,$0-1
|
||||||
|
MOVQ BX, R8 // save BX (callee-saved, clobbered by CPUID)
|
||||||
|
|
||||||
|
// Check max CPUID leaf >= 7
|
||||||
|
MOVL $0, AX
|
||||||
|
CPUID
|
||||||
|
CMPL AX, $7
|
||||||
|
JL cpuid_no
|
||||||
|
|
||||||
|
// Check OSXSAVE (CPUID.1:ECX bit 27)
|
||||||
|
MOVL $1, AX
|
||||||
|
CPUID
|
||||||
|
TESTL $(1<<27), CX
|
||||||
|
JZ cpuid_no
|
||||||
|
|
||||||
|
// Check AVX2 (CPUID.7.0:EBX bit 5)
|
||||||
|
MOVL $7, AX
|
||||||
|
MOVL $0, CX
|
||||||
|
CPUID
|
||||||
|
TESTL $(1<<5), BX
|
||||||
|
JZ cpuid_no
|
||||||
|
|
||||||
|
// Check OS YMM state support via XGETBV
|
||||||
|
MOVL $0, CX
|
||||||
|
BYTE $0x0F; BYTE $0x01; BYTE $0xD0 // XGETBV → EDX:EAX
|
||||||
|
ANDL $6, AX // bits 1 (XMM) and 2 (YMM)
|
||||||
|
CMPL AX, $6
|
||||||
|
JNE cpuid_no
|
||||||
|
|
||||||
|
MOVQ R8, BX // restore BX
|
||||||
|
MOVB $1, ret+0(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
cpuid_no:
|
||||||
|
MOVQ R8, BX
|
||||||
|
MOVB $0, ret+0(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func indexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
//
|
||||||
|
// Returns the index of the first occurrence of b1 or b2 in s, or -1.
|
||||||
|
// Uses AVX2 (32 bytes/iter) when available, SSE2 (16 bytes/iter) otherwise.
|
||||||
|
TEXT ·indexByteTwo(SB),NOSPLIT,$0-40
|
||||||
|
MOVQ s_base+0(FP), SI
|
||||||
|
MOVQ s_len+8(FP), BX
|
||||||
|
MOVBLZX b1+24(FP), AX
|
||||||
|
MOVBLZX b2+25(FP), CX
|
||||||
|
LEAQ ret+32(FP), R8
|
||||||
|
|
||||||
|
TESTQ BX, BX
|
||||||
|
JEQ fwd_failure
|
||||||
|
|
||||||
|
// Try AVX2 for inputs >= 32 bytes
|
||||||
|
CMPQ BX, $32
|
||||||
|
JLT fwd_sse2
|
||||||
|
CMPB ·_useAVX2(SB), $1
|
||||||
|
JNE fwd_sse2
|
||||||
|
|
||||||
|
// ====== AVX2 forward search ======
|
||||||
|
MOVD AX, X0
|
||||||
|
VPBROADCASTB X0, Y0 // Y0 = splat(b1)
|
||||||
|
MOVD CX, X1
|
||||||
|
VPBROADCASTB X1, Y1 // Y1 = splat(b2)
|
||||||
|
|
||||||
|
MOVQ SI, DI
|
||||||
|
LEAQ -32(SI)(BX*1), AX // AX = last valid 32-byte chunk
|
||||||
|
JMP fwd_avx2_entry
|
||||||
|
|
||||||
|
fwd_avx2_loop:
|
||||||
|
VMOVDQU (DI), Y2
|
||||||
|
VPCMPEQB Y0, Y2, Y3
|
||||||
|
VPCMPEQB Y1, Y2, Y4
|
||||||
|
VPOR Y3, Y4, Y3
|
||||||
|
VPMOVMSKB Y3, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JNZ fwd_avx2_success
|
||||||
|
ADDQ $32, DI
|
||||||
|
|
||||||
|
fwd_avx2_entry:
|
||||||
|
CMPQ DI, AX
|
||||||
|
JB fwd_avx2_loop
|
||||||
|
|
||||||
|
// Last 32-byte chunk (may overlap with previous)
|
||||||
|
MOVQ AX, DI
|
||||||
|
VMOVDQU (AX), Y2
|
||||||
|
VPCMPEQB Y0, Y2, Y3
|
||||||
|
VPCMPEQB Y1, Y2, Y4
|
||||||
|
VPOR Y3, Y4, Y3
|
||||||
|
VPMOVMSKB Y3, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JNZ fwd_avx2_success
|
||||||
|
|
||||||
|
MOVQ $-1, (R8)
|
||||||
|
VZEROUPPER
|
||||||
|
RET
|
||||||
|
|
||||||
|
fwd_avx2_success:
|
||||||
|
SUBQ SI, DI
|
||||||
|
ADDQ DX, DI
|
||||||
|
MOVQ DI, (R8)
|
||||||
|
VZEROUPPER
|
||||||
|
RET
|
||||||
|
|
||||||
|
// ====== SSE2 forward search (< 32 bytes or no AVX2) ======
|
||||||
|
|
||||||
|
fwd_sse2:
|
||||||
|
// Broadcast b1 into X0
|
||||||
|
MOVD AX, X0
|
||||||
|
PUNPCKLBW X0, X0
|
||||||
|
PUNPCKLBW X0, X0
|
||||||
|
PSHUFL $0, X0, X0
|
||||||
|
|
||||||
|
// Broadcast b2 into X4
|
||||||
|
MOVD CX, X4
|
||||||
|
PUNPCKLBW X4, X4
|
||||||
|
PUNPCKLBW X4, X4
|
||||||
|
PSHUFL $0, X4, X4
|
||||||
|
|
||||||
|
CMPQ BX, $16
|
||||||
|
JLT fwd_small
|
||||||
|
|
||||||
|
MOVQ SI, DI
|
||||||
|
LEAQ -16(SI)(BX*1), AX
|
||||||
|
JMP fwd_sseloopentry
|
||||||
|
|
||||||
|
fwd_sseloop:
|
||||||
|
MOVOU (DI), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JNZ fwd_ssesuccess
|
||||||
|
ADDQ $16, DI
|
||||||
|
|
||||||
|
fwd_sseloopentry:
|
||||||
|
CMPQ DI, AX
|
||||||
|
JB fwd_sseloop
|
||||||
|
|
||||||
|
// Search the last 16-byte chunk (may overlap)
|
||||||
|
MOVQ AX, DI
|
||||||
|
MOVOU (AX), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JNZ fwd_ssesuccess
|
||||||
|
|
||||||
|
fwd_failure:
|
||||||
|
MOVQ $-1, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
fwd_ssesuccess:
|
||||||
|
SUBQ SI, DI
|
||||||
|
ADDQ DX, DI
|
||||||
|
MOVQ DI, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
fwd_small:
|
||||||
|
// Check if loading 16 bytes from SI would cross a page boundary
|
||||||
|
LEAQ 16(SI), AX
|
||||||
|
TESTW $0xff0, AX
|
||||||
|
JEQ fwd_endofpage
|
||||||
|
|
||||||
|
MOVOU (SI), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JZ fwd_failure
|
||||||
|
CMPL DX, BX
|
||||||
|
JAE fwd_failure
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
fwd_endofpage:
|
||||||
|
MOVOU -16(SI)(BX*1), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
MOVL BX, CX
|
||||||
|
SHLL CX, DX
|
||||||
|
SHRL $16, DX
|
||||||
|
BSFL DX, DX
|
||||||
|
JZ fwd_failure
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
//
|
||||||
|
// Returns the index of the last occurrence of b1 or b2 in s, or -1.
|
||||||
|
// Uses AVX2 (32 bytes/iter) when available, SSE2 (16 bytes/iter) otherwise.
|
||||||
|
TEXT ·lastIndexByteTwo(SB),NOSPLIT,$0-40
|
||||||
|
MOVQ s_base+0(FP), SI
|
||||||
|
MOVQ s_len+8(FP), BX
|
||||||
|
MOVBLZX b1+24(FP), AX
|
||||||
|
MOVBLZX b2+25(FP), CX
|
||||||
|
LEAQ ret+32(FP), R8
|
||||||
|
|
||||||
|
TESTQ BX, BX
|
||||||
|
JEQ back_failure
|
||||||
|
|
||||||
|
// Try AVX2 for inputs >= 32 bytes
|
||||||
|
CMPQ BX, $32
|
||||||
|
JLT back_sse2
|
||||||
|
CMPB ·_useAVX2(SB), $1
|
||||||
|
JNE back_sse2
|
||||||
|
|
||||||
|
// ====== AVX2 backward search ======
|
||||||
|
MOVD AX, X0
|
||||||
|
VPBROADCASTB X0, Y0
|
||||||
|
MOVD CX, X1
|
||||||
|
VPBROADCASTB X1, Y1
|
||||||
|
|
||||||
|
// DI = start of last 32-byte chunk
|
||||||
|
LEAQ -32(SI)(BX*1), DI
|
||||||
|
|
||||||
|
back_avx2_loop:
|
||||||
|
CMPQ DI, SI
|
||||||
|
JBE back_avx2_first
|
||||||
|
|
||||||
|
VMOVDQU (DI), Y2
|
||||||
|
VPCMPEQB Y0, Y2, Y3
|
||||||
|
VPCMPEQB Y1, Y2, Y4
|
||||||
|
VPOR Y3, Y4, Y3
|
||||||
|
VPMOVMSKB Y3, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JNZ back_avx2_success
|
||||||
|
SUBQ $32, DI
|
||||||
|
JMP back_avx2_loop
|
||||||
|
|
||||||
|
back_avx2_first:
|
||||||
|
// First 32 bytes (DI <= SI, load from SI)
|
||||||
|
VMOVDQU (SI), Y2
|
||||||
|
VPCMPEQB Y0, Y2, Y3
|
||||||
|
VPCMPEQB Y1, Y2, Y4
|
||||||
|
VPOR Y3, Y4, Y3
|
||||||
|
VPMOVMSKB Y3, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JNZ back_avx2_firstsuccess
|
||||||
|
|
||||||
|
MOVQ $-1, (R8)
|
||||||
|
VZEROUPPER
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_avx2_success:
|
||||||
|
SUBQ SI, DI
|
||||||
|
ADDQ DX, DI
|
||||||
|
MOVQ DI, (R8)
|
||||||
|
VZEROUPPER
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_avx2_firstsuccess:
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
VZEROUPPER
|
||||||
|
RET
|
||||||
|
|
||||||
|
// ====== SSE2 backward search (< 32 bytes or no AVX2) ======
|
||||||
|
|
||||||
|
back_sse2:
|
||||||
|
// Broadcast b1 into X0
|
||||||
|
MOVD AX, X0
|
||||||
|
PUNPCKLBW X0, X0
|
||||||
|
PUNPCKLBW X0, X0
|
||||||
|
PSHUFL $0, X0, X0
|
||||||
|
|
||||||
|
// Broadcast b2 into X4
|
||||||
|
MOVD CX, X4
|
||||||
|
PUNPCKLBW X4, X4
|
||||||
|
PUNPCKLBW X4, X4
|
||||||
|
PSHUFL $0, X4, X4
|
||||||
|
|
||||||
|
CMPQ BX, $16
|
||||||
|
JLT back_small
|
||||||
|
|
||||||
|
// DI = start of last 16-byte chunk
|
||||||
|
LEAQ -16(SI)(BX*1), DI
|
||||||
|
|
||||||
|
back_sseloop:
|
||||||
|
CMPQ DI, SI
|
||||||
|
JBE back_ssefirst
|
||||||
|
|
||||||
|
MOVOU (DI), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JNZ back_ssesuccess
|
||||||
|
SUBQ $16, DI
|
||||||
|
JMP back_sseloop
|
||||||
|
|
||||||
|
back_ssefirst:
|
||||||
|
// First 16 bytes (DI <= SI, load from SI)
|
||||||
|
MOVOU (SI), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JNZ back_ssefirstsuccess
|
||||||
|
|
||||||
|
back_failure:
|
||||||
|
MOVQ $-1, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_ssesuccess:
|
||||||
|
SUBQ SI, DI
|
||||||
|
ADDQ DX, DI
|
||||||
|
MOVQ DI, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_ssefirstsuccess:
|
||||||
|
// DX = byte offset from base
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_small:
|
||||||
|
// Check page boundary
|
||||||
|
LEAQ 16(SI), AX
|
||||||
|
TESTW $0xff0, AX
|
||||||
|
JEQ back_endofpage
|
||||||
|
|
||||||
|
MOVOU (SI), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
// Mask to first BX bytes: keep bits 0..BX-1
|
||||||
|
MOVL $1, AX
|
||||||
|
MOVL BX, CX
|
||||||
|
SHLL CX, AX
|
||||||
|
DECL AX
|
||||||
|
ANDL AX, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JZ back_failure
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
back_endofpage:
|
||||||
|
// Load 16 bytes ending at base+n
|
||||||
|
MOVOU -16(SI)(BX*1), X1
|
||||||
|
MOVOU X1, X2
|
||||||
|
PCMPEQB X0, X1
|
||||||
|
PCMPEQB X4, X2
|
||||||
|
POR X2, X1
|
||||||
|
PMOVMSKB X1, DX
|
||||||
|
// Bits correspond to bytes [base+n-16, base+n).
|
||||||
|
// We want original bytes [0, n), which are bits [16-n, 16).
|
||||||
|
// Mask: keep bits (16-n) through 15.
|
||||||
|
MOVL $16, CX
|
||||||
|
SUBL BX, CX
|
||||||
|
SHRL CX, DX
|
||||||
|
SHLL CX, DX
|
||||||
|
BSRL DX, DX
|
||||||
|
JZ back_failure
|
||||||
|
// DX is the bit position in the loaded chunk.
|
||||||
|
// Original byte index = DX - (16 - n) = DX + n - 16
|
||||||
|
ADDL BX, DX
|
||||||
|
SUBL $16, DX
|
||||||
|
MOVQ DX, (R8)
|
||||||
|
RET
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build arm64
|
||||||
|
|
||||||
|
package algo
|
||||||
|
|
||||||
|
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present. Implemented in assembly using ARM64 NEON
|
||||||
|
// to search for both bytes in a single pass.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func indexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
|
||||||
|
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present. Implemented in assembly using ARM64 NEON,
|
||||||
|
// scanning backward.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func indexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
//
|
||||||
|
// Returns the index of the first occurrence of b1 or b2 in s, or -1.
|
||||||
|
// Uses ARM64 NEON to search for both bytes in a single pass over the data.
|
||||||
|
// Adapted from Go's internal/bytealg/indexbyte_arm64.s (single-byte version).
|
||||||
|
TEXT ·indexByteTwo(SB),NOSPLIT,$0-40
|
||||||
|
MOVD s_base+0(FP), R0
|
||||||
|
MOVD s_len+8(FP), R2
|
||||||
|
MOVBU b1+24(FP), R1
|
||||||
|
MOVBU b2+25(FP), R7
|
||||||
|
MOVD $ret+32(FP), R8
|
||||||
|
|
||||||
|
// Core algorithm:
|
||||||
|
// For each 32-byte chunk we calculate a 64-bit syndrome value,
|
||||||
|
// with two bits per byte. We compare against both b1 and b2,
|
||||||
|
// OR the results, then use the same syndrome extraction as
|
||||||
|
// Go's IndexByte.
|
||||||
|
|
||||||
|
CBZ R2, fail
|
||||||
|
MOVD R0, R11
|
||||||
|
// Magic constant 0x40100401 allows us to identify which lane matches.
|
||||||
|
// Each byte in the group of 4 gets a distinct bit: 1, 4, 16, 64.
|
||||||
|
MOVD $0x40100401, R5
|
||||||
|
VMOV R1, V0.B16 // V0 = splat(b1)
|
||||||
|
VMOV R7, V7.B16 // V7 = splat(b2)
|
||||||
|
// Work with aligned 32-byte chunks
|
||||||
|
BIC $0x1f, R0, R3
|
||||||
|
VMOV R5, V5.S4
|
||||||
|
ANDS $0x1f, R0, R9
|
||||||
|
AND $0x1f, R2, R10
|
||||||
|
BEQ loop
|
||||||
|
|
||||||
|
// Input string is not 32-byte aligned. Process the first
|
||||||
|
// aligned 32-byte block and mask off bytes before our start.
|
||||||
|
VLD1.P (R3), [V1.B16, V2.B16]
|
||||||
|
SUB $0x20, R9, R4
|
||||||
|
ADDS R4, R2, R2
|
||||||
|
// Compare against both needles
|
||||||
|
VCMEQ V0.B16, V1.B16, V3.B16 // b1 vs first 16 bytes
|
||||||
|
VCMEQ V7.B16, V1.B16, V8.B16 // b2 vs first 16 bytes
|
||||||
|
VORR V8.B16, V3.B16, V3.B16 // combine
|
||||||
|
VCMEQ V0.B16, V2.B16, V4.B16 // b1 vs second 16 bytes
|
||||||
|
VCMEQ V7.B16, V2.B16, V9.B16 // b2 vs second 16 bytes
|
||||||
|
VORR V9.B16, V4.B16, V4.B16 // combine
|
||||||
|
// Build syndrome
|
||||||
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.B16, V6.B16, V6.B16
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
// Clear the irrelevant lower bits
|
||||||
|
LSL $1, R9, R4
|
||||||
|
LSR R4, R6, R6
|
||||||
|
LSL R4, R6, R6
|
||||||
|
// The first block can also be the last
|
||||||
|
BLS masklast
|
||||||
|
// Have we found something already?
|
||||||
|
CBNZ R6, tail
|
||||||
|
|
||||||
|
loop:
|
||||||
|
VLD1.P (R3), [V1.B16, V2.B16]
|
||||||
|
SUBS $0x20, R2, R2
|
||||||
|
// Compare against both needles, OR results
|
||||||
|
VCMEQ V0.B16, V1.B16, V3.B16
|
||||||
|
VCMEQ V7.B16, V1.B16, V8.B16
|
||||||
|
VORR V8.B16, V3.B16, V3.B16
|
||||||
|
VCMEQ V0.B16, V2.B16, V4.B16
|
||||||
|
VCMEQ V7.B16, V2.B16, V9.B16
|
||||||
|
VORR V9.B16, V4.B16, V4.B16
|
||||||
|
// If we're out of data we finish regardless of the result
|
||||||
|
BLS end
|
||||||
|
// Fast check: OR both halves and check for any match
|
||||||
|
VORR V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.D2, V6.D2, V6.D2
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
CBZ R6, loop
|
||||||
|
|
||||||
|
end:
|
||||||
|
// Found something or out of data — build full syndrome
|
||||||
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.B16, V6.B16, V6.B16
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
// Only mask for the last block
|
||||||
|
BHS tail
|
||||||
|
|
||||||
|
masklast:
|
||||||
|
// Clear irrelevant upper bits
|
||||||
|
ADD R9, R10, R4
|
||||||
|
AND $0x1f, R4, R4
|
||||||
|
SUB $0x20, R4, R4
|
||||||
|
NEG R4<<1, R4
|
||||||
|
LSL R4, R6, R6
|
||||||
|
LSR R4, R6, R6
|
||||||
|
|
||||||
|
tail:
|
||||||
|
CBZ R6, fail
|
||||||
|
RBIT R6, R6
|
||||||
|
SUB $0x20, R3, R3
|
||||||
|
CLZ R6, R6
|
||||||
|
ADD R6>>1, R3, R0
|
||||||
|
SUB R11, R0, R0
|
||||||
|
MOVD R0, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
fail:
|
||||||
|
MOVD $-1, R0
|
||||||
|
MOVD R0, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||||
|
//
|
||||||
|
// Returns the index of the last occurrence of b1 or b2 in s, or -1.
|
||||||
|
// Scans backward using ARM64 NEON.
|
||||||
|
TEXT ·lastIndexByteTwo(SB),NOSPLIT,$0-40
|
||||||
|
MOVD s_base+0(FP), R0
|
||||||
|
MOVD s_len+8(FP), R2
|
||||||
|
MOVBU b1+24(FP), R1
|
||||||
|
MOVBU b2+25(FP), R7
|
||||||
|
MOVD $ret+32(FP), R8
|
||||||
|
|
||||||
|
CBZ R2, lfail
|
||||||
|
MOVD R0, R11 // save base
|
||||||
|
ADD R0, R2, R12 // R12 = end = base + len
|
||||||
|
MOVD $0x40100401, R5
|
||||||
|
VMOV R1, V0.B16 // V0 = splat(b1)
|
||||||
|
VMOV R7, V7.B16 // V7 = splat(b2)
|
||||||
|
VMOV R5, V5.S4
|
||||||
|
|
||||||
|
// Align: find the aligned block containing the last byte
|
||||||
|
SUB $1, R12, R3
|
||||||
|
BIC $0x1f, R3, R3 // R3 = start of aligned block containing last byte
|
||||||
|
|
||||||
|
// --- Process tail block ---
|
||||||
|
VLD1 (R3), [V1.B16, V2.B16]
|
||||||
|
VCMEQ V0.B16, V1.B16, V3.B16
|
||||||
|
VCMEQ V7.B16, V1.B16, V8.B16
|
||||||
|
VORR V8.B16, V3.B16, V3.B16
|
||||||
|
VCMEQ V0.B16, V2.B16, V4.B16
|
||||||
|
VCMEQ V7.B16, V2.B16, V9.B16
|
||||||
|
VORR V9.B16, V4.B16, V4.B16
|
||||||
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.B16, V6.B16, V6.B16
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
|
||||||
|
// Mask upper bits (bytes past end of slice)
|
||||||
|
// tail_bytes = end - R3 (1..32)
|
||||||
|
SUB R3, R12, R10 // R10 = tail_bytes
|
||||||
|
MOVD $64, R4
|
||||||
|
SUB R10<<1, R4, R4 // R4 = 64 - 2*tail_bytes
|
||||||
|
LSL R4, R6, R6
|
||||||
|
LSR R4, R6, R6
|
||||||
|
|
||||||
|
// Is this also the head block?
|
||||||
|
CMP R11, R3 // R3 - R11
|
||||||
|
BLO lmaskfirst // R3 < base: head+tail in same block
|
||||||
|
BEQ ltailonly // R3 == base: single aligned block
|
||||||
|
|
||||||
|
// R3 > base: more blocks before this one
|
||||||
|
CBNZ R6, llast
|
||||||
|
B lbacksetup
|
||||||
|
|
||||||
|
ltailonly:
|
||||||
|
// Single block, already masked upper bits
|
||||||
|
CBNZ R6, llast
|
||||||
|
B lfail
|
||||||
|
|
||||||
|
lmaskfirst:
|
||||||
|
// Mask lower bits (bytes before start of slice)
|
||||||
|
SUB R3, R11, R4 // R4 = base - R3
|
||||||
|
LSL $1, R4, R4
|
||||||
|
LSR R4, R6, R6
|
||||||
|
LSL R4, R6, R6
|
||||||
|
CBNZ R6, llast
|
||||||
|
B lfail
|
||||||
|
|
||||||
|
lbacksetup:
|
||||||
|
SUB $0x20, R3
|
||||||
|
|
||||||
|
lbackloop:
|
||||||
|
VLD1 (R3), [V1.B16, V2.B16]
|
||||||
|
VCMEQ V0.B16, V1.B16, V3.B16
|
||||||
|
VCMEQ V7.B16, V1.B16, V8.B16
|
||||||
|
VORR V8.B16, V3.B16, V3.B16
|
||||||
|
VCMEQ V0.B16, V2.B16, V4.B16
|
||||||
|
VCMEQ V7.B16, V2.B16, V9.B16
|
||||||
|
VORR V9.B16, V4.B16, V4.B16
|
||||||
|
// Quick check: any match in this block?
|
||||||
|
VORR V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.D2, V6.D2, V6.D2
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
|
||||||
|
// Is this a head block? (R3 < base)
|
||||||
|
CMP R11, R3
|
||||||
|
BLO lheadblock
|
||||||
|
|
||||||
|
// Full block (R3 >= base)
|
||||||
|
CBNZ R6, lbackfound
|
||||||
|
// More blocks?
|
||||||
|
BEQ lfail // R3 == base, no more
|
||||||
|
SUB $0x20, R3
|
||||||
|
B lbackloop
|
||||||
|
|
||||||
|
lbackfound:
|
||||||
|
// Build full syndrome
|
||||||
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.B16, V6.B16, V6.B16
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
B llast
|
||||||
|
|
||||||
|
lheadblock:
|
||||||
|
// R3 < base. Build full syndrome if quick check had a match.
|
||||||
|
CBZ R6, lfail
|
||||||
|
VAND V5.B16, V3.B16, V3.B16
|
||||||
|
VAND V5.B16, V4.B16, V4.B16
|
||||||
|
VADDP V4.B16, V3.B16, V6.B16
|
||||||
|
VADDP V6.B16, V6.B16, V6.B16
|
||||||
|
VMOV V6.D[0], R6
|
||||||
|
// Mask lower bits
|
||||||
|
SUB R3, R11, R4 // R4 = base - R3
|
||||||
|
LSL $1, R4, R4
|
||||||
|
LSR R4, R6, R6
|
||||||
|
LSL R4, R6, R6
|
||||||
|
CBZ R6, lfail
|
||||||
|
|
||||||
|
llast:
|
||||||
|
// Find last match: highest set bit in syndrome
|
||||||
|
// Syndrome has bit 2i set for matching byte i.
|
||||||
|
// CLZ gives leading zeros; byte_offset = (63 - CLZ) / 2.
|
||||||
|
CLZ R6, R6
|
||||||
|
MOVD $63, R4
|
||||||
|
SUB R6, R4, R6 // R6 = 63 - CLZ = bit position
|
||||||
|
LSR $1, R6 // R6 = byte offset within block
|
||||||
|
ADD R3, R6, R0 // R0 = absolute address
|
||||||
|
SUB R11, R0, R0 // R0 = slice index
|
||||||
|
MOVD R0, (R8)
|
||||||
|
RET
|
||||||
|
|
||||||
|
lfail:
|
||||||
|
MOVD $-1, R0
|
||||||
|
MOVD R0, (R8)
|
||||||
|
RET
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//go:build !arm64 && !amd64
|
||||||
|
|
||||||
|
package algo
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present.
|
||||||
|
func indexByteTwo(s []byte, b1, b2 byte) int {
|
||||||
|
i1 := bytes.IndexByte(s, b1)
|
||||||
|
if i1 == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
scope := s
|
||||||
|
if i1 > 0 {
|
||||||
|
scope = s[:i1]
|
||||||
|
}
|
||||||
|
if i2 := bytes.IndexByte(scope, b2); i2 >= 0 {
|
||||||
|
return i2
|
||||||
|
}
|
||||||
|
return i1
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||||
|
// or -1 if neither is present.
|
||||||
|
func lastIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if s[i] == b1 || s[i] == b2 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package algo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIndexByteTwo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
s string
|
||||||
|
b1 byte
|
||||||
|
b2 byte
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"empty", "", 'a', 'b', -1},
|
||||||
|
{"single_b1", "a", 'a', 'b', 0},
|
||||||
|
{"single_b2", "b", 'a', 'b', 0},
|
||||||
|
{"single_none", "c", 'a', 'b', -1},
|
||||||
|
{"b1_first", "xaxb", 'a', 'b', 1},
|
||||||
|
{"b2_first", "xbxa", 'a', 'b', 1},
|
||||||
|
{"same_byte", "xxa", 'a', 'a', 2},
|
||||||
|
{"at_end", "xxxxa", 'a', 'b', 4},
|
||||||
|
{"not_found", "xxxxxxxx", 'a', 'b', -1},
|
||||||
|
{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||||
|
{"long_b2_at_3000", string(make([]byte, 3000)) + "b" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := indexByteTwo([]byte(tt.s), tt.b1, tt.b2)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("indexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exhaustive test: compare against loop reference for various lengths,
|
||||||
|
// including sizes around SIMD block boundaries (16, 32, 64).
|
||||||
|
for n := 0; n <= 256; n++ {
|
||||||
|
data := make([]byte, n)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte('c' + (i % 20))
|
||||||
|
}
|
||||||
|
// Test with match at every position
|
||||||
|
for pos := 0; pos < n; pos++ {
|
||||||
|
for _, b := range []byte{'A', 'B'} {
|
||||||
|
data[pos] = b
|
||||||
|
got := indexByteTwo(data, 'A', 'B')
|
||||||
|
want := loopIndexByteTwo(data, 'A', 'B')
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("indexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
|
||||||
|
}
|
||||||
|
data[pos] = byte('c' + (pos % 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Test with no match
|
||||||
|
got := indexByteTwo(data, 'A', 'B')
|
||||||
|
if got != -1 {
|
||||||
|
t.Fatalf("indexByteTwo(len=%d, no match) = %d, want -1", n, got)
|
||||||
|
}
|
||||||
|
// Test with both bytes present
|
||||||
|
if n >= 2 {
|
||||||
|
data[n/3] = 'A'
|
||||||
|
data[n*2/3] = 'B'
|
||||||
|
got := indexByteTwo(data, 'A', 'B')
|
||||||
|
want := loopIndexByteTwo(data, 'A', 'B')
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("indexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
|
||||||
|
}
|
||||||
|
data[n/3] = byte('c' + ((n / 3) % 20))
|
||||||
|
data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastIndexByteTwo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
s string
|
||||||
|
b1 byte
|
||||||
|
b2 byte
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"empty", "", 'a', 'b', -1},
|
||||||
|
{"single_b1", "a", 'a', 'b', 0},
|
||||||
|
{"single_b2", "b", 'a', 'b', 0},
|
||||||
|
{"single_none", "c", 'a', 'b', -1},
|
||||||
|
{"b1_last", "xbxa", 'a', 'b', 3},
|
||||||
|
{"b2_last", "xaxb", 'a', 'b', 3},
|
||||||
|
{"same_byte", "axx", 'a', 'a', 0},
|
||||||
|
{"at_start", "axxxx", 'a', 'b', 0},
|
||||||
|
{"both_present", "axbx", 'a', 'b', 2},
|
||||||
|
{"not_found", "xxxxxxxx", 'a', 'b', -1},
|
||||||
|
{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||||
|
{"long_b2_at_end", string(make([]byte, 4000)) + "b", 'a', 'b', 4000},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := lastIndexByteTwo([]byte(tt.s), tt.b1, tt.b2)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("lastIndexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exhaustive test against loop reference
|
||||||
|
for n := 0; n <= 256; n++ {
|
||||||
|
data := make([]byte, n)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte('c' + (i % 20))
|
||||||
|
}
|
||||||
|
for pos := 0; pos < n; pos++ {
|
||||||
|
for _, b := range []byte{'A', 'B'} {
|
||||||
|
data[pos] = b
|
||||||
|
got := lastIndexByteTwo(data, 'A', 'B')
|
||||||
|
want := refLastIndexByteTwo(data, 'A', 'B')
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("lastIndexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
|
||||||
|
}
|
||||||
|
data[pos] = byte('c' + (pos % 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No match
|
||||||
|
got := lastIndexByteTwo(data, 'A', 'B')
|
||||||
|
if got != -1 {
|
||||||
|
t.Fatalf("lastIndexByteTwo(len=%d, no match) = %d, want -1", n, got)
|
||||||
|
}
|
||||||
|
// Both bytes present
|
||||||
|
if n >= 2 {
|
||||||
|
data[n/3] = 'A'
|
||||||
|
data[n*2/3] = 'B'
|
||||||
|
got := lastIndexByteTwo(data, 'A', 'B')
|
||||||
|
want := refLastIndexByteTwo(data, 'A', 'B')
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("lastIndexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
|
||||||
|
}
|
||||||
|
data[n/3] = byte('c' + ((n / 3) % 20))
|
||||||
|
data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzIndexByteTwo(f *testing.F) {
|
||||||
|
f.Add([]byte("hello world"), byte('o'), byte('l'))
|
||||||
|
f.Add([]byte(""), byte('a'), byte('b'))
|
||||||
|
f.Add([]byte("aaa"), byte('a'), byte('a'))
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
|
||||||
|
got := indexByteTwo(data, b1, b2)
|
||||||
|
want := loopIndexByteTwo(data, b1, b2)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("indexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzLastIndexByteTwo(f *testing.F) {
|
||||||
|
f.Add([]byte("hello world"), byte('o'), byte('l'))
|
||||||
|
f.Add([]byte(""), byte('a'), byte('b'))
|
||||||
|
f.Add([]byte("aaa"), byte('a'), byte('a'))
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
|
||||||
|
got := lastIndexByteTwo(data, b1, b2)
|
||||||
|
want := refLastIndexByteTwo(data, b1, b2)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("lastIndexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference implementations for correctness checking
|
||||||
|
func refIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||||
|
i1 := bytes.IndexByte(s, b1)
|
||||||
|
if i1 == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
scope := s
|
||||||
|
if i1 > 0 {
|
||||||
|
scope = s[:i1]
|
||||||
|
}
|
||||||
|
if i2 := bytes.IndexByte(scope, b2); i2 >= 0 {
|
||||||
|
return i2
|
||||||
|
}
|
||||||
|
return i1
|
||||||
|
}
|
||||||
|
|
||||||
|
func loopIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||||
|
for i, b := range s {
|
||||||
|
if b == b1 || b == b2 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func refLastIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if s[i] == b1 || s[i] == b2 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchIndexByteTwo(b *testing.B, size int, pos int) {
|
||||||
|
data := make([]byte, size)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte('a' + (i % 20))
|
||||||
|
}
|
||||||
|
data[pos] = 'Z'
|
||||||
|
|
||||||
|
type impl struct {
|
||||||
|
name string
|
||||||
|
fn func([]byte, byte, byte) int
|
||||||
|
}
|
||||||
|
impls := []impl{
|
||||||
|
{"asm", indexByteTwo},
|
||||||
|
{"2xIndexByte", refIndexByteTwo},
|
||||||
|
{"loop", loopIndexByteTwo},
|
||||||
|
}
|
||||||
|
for _, im := range impls {
|
||||||
|
b.Run(im.name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
im.fn(data, 'Z', 'z')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchLastIndexByteTwo(b *testing.B, size int, pos int) {
|
||||||
|
data := make([]byte, size)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte('a' + (i % 20))
|
||||||
|
}
|
||||||
|
data[pos] = 'Z'
|
||||||
|
|
||||||
|
type impl struct {
|
||||||
|
name string
|
||||||
|
fn func([]byte, byte, byte) int
|
||||||
|
}
|
||||||
|
impls := []impl{
|
||||||
|
{"asm", lastIndexByteTwo},
|
||||||
|
{"loop", refLastIndexByteTwo},
|
||||||
|
}
|
||||||
|
for _, im := range impls {
|
||||||
|
b.Run(im.name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
im.fn(data, 'Z', 'z')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIndexByteTwo_10(b *testing.B) { benchIndexByteTwo(b, 10, 8) }
|
||||||
|
func BenchmarkIndexByteTwo_100(b *testing.B) { benchIndexByteTwo(b, 100, 80) }
|
||||||
|
func BenchmarkIndexByteTwo_1000(b *testing.B) { benchIndexByteTwo(b, 1000, 800) }
|
||||||
|
func BenchmarkLastIndexByteTwo_10(b *testing.B) { benchLastIndexByteTwo(b, 10, 2) }
|
||||||
|
func BenchmarkLastIndexByteTwo_100(b *testing.B) { benchLastIndexByteTwo(b, 100, 20) }
|
||||||
|
func BenchmarkLastIndexByteTwo_1000(b *testing.B) { benchLastIndexByteTwo(b, 1000, 200) }
|
||||||
@@ -52,6 +52,20 @@ func (cl *ChunkList) lastChunk() *Chunk {
|
|||||||
return cl.chunks[len(cl.chunks)-1]
|
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
|
// CountItems returns the total number of Items
|
||||||
func CountItems(cs []*Chunk) int {
|
func CountItems(cs []*Chunk) int {
|
||||||
if len(cs) == 0 {
|
if len(cs) == 0 {
|
||||||
@@ -85,6 +99,21 @@ func (cl *ChunkList) Clear() {
|
|||||||
cl.mutex.Unlock()
|
cl.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForEachItem iterates all items and applies fn to each one.
|
||||||
|
// The done callback runs under the lock to safely update shared state.
|
||||||
|
func (cl *ChunkList) ForEachItem(fn func(*Item), done func()) {
|
||||||
|
cl.mutex.Lock()
|
||||||
|
for _, chunk := range cl.chunks {
|
||||||
|
for i := 0; i < chunk.count; i++ {
|
||||||
|
fn(&chunk.items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done != nil {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
cl.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot returns immutable snapshot of the ChunkList
|
// Snapshot returns immutable snapshot of the ChunkList
|
||||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||||
cl.mutex.Lock()
|
cl.mutex.Lock()
|
||||||
|
|||||||
+1
-2
@@ -39,7 +39,7 @@ const (
|
|||||||
progressMinDuration = 200 * time.Millisecond
|
progressMinDuration = 200 * time.Millisecond
|
||||||
|
|
||||||
// Capacity of each chunk
|
// Capacity of each chunk
|
||||||
chunkSize int = 100
|
chunkSize int = 1000
|
||||||
|
|
||||||
// Pre-allocated memory slices to minimize GC
|
// Pre-allocated memory slices to minimize GC
|
||||||
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
||||||
@@ -65,7 +65,6 @@ const (
|
|||||||
EvtSearchNew
|
EvtSearchNew
|
||||||
EvtSearchProgress
|
EvtSearchProgress
|
||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
EvtHeader
|
|
||||||
EvtReady
|
EvtReady
|
||||||
EvtQuit
|
EvtQuit
|
||||||
)
|
)
|
||||||
|
|||||||
+146
-55
@@ -2,6 +2,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -17,7 +18,6 @@ Reader -> EvtReadNew -> Matcher (restart)
|
|||||||
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
||||||
Matcher -> EvtSearchProgress -> Terminal (update info)
|
Matcher -> EvtSearchProgress -> Terminal (update info)
|
||||||
Matcher -> EvtSearchFin -> Terminal (update list)
|
Matcher -> EvtSearchFin -> Terminal (update list)
|
||||||
Matcher -> EvtHeader -> Terminal (update header)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type revision struct {
|
type revision struct {
|
||||||
@@ -113,57 +113,57 @@ func Run(opts *Options) (int, error) {
|
|||||||
cache := NewChunkCache()
|
cache := NewChunkCache()
|
||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
// transformItem applies with-nth transformation to an item's raw data.
|
||||||
|
// It handles ANSI token propagation using prevLineAnsiState for cross-line continuity.
|
||||||
|
transformItem := func(item *Item, data []byte, transformer func([]Token, int32) string, index int32) {
|
||||||
|
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||||
|
if opts.Ansi && len(tokens) > 1 {
|
||||||
|
var ansiState *ansiState
|
||||||
|
if prevLineAnsiState != nil {
|
||||||
|
ansiStateDup := *prevLineAnsiState
|
||||||
|
ansiState = &ansiStateDup
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
prevAnsiState := ansiState
|
||||||
|
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
|
||||||
|
if prevAnsiState != nil {
|
||||||
|
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
|
||||||
|
} else {
|
||||||
|
token.text.Prepend("\x1b[m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transformed := transformer(tokens, index)
|
||||||
|
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||||
|
|
||||||
|
// We should not trim trailing whitespaces with background colors
|
||||||
|
var maxColorOffset int32
|
||||||
|
if item.colors != nil {
|
||||||
|
for _, ansi := range *item.colors {
|
||||||
|
if ansi.color.bg >= 0 {
|
||||||
|
maxColorOffset = max(maxColorOffset, ansi.offset[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
var nthTransformer func([]Token, int32) string
|
||||||
if opts.WithNth == nil {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
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, item.colors = ansiProcessor(data)
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
itemIndex++
|
itemIndex++
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
nthTransformer = opts.WithNth(opts.Delimiter)
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
if nthTransformer == nil {
|
||||||
if opts.Ansi && len(tokens) > 1 {
|
item.text, item.colors = ansiProcessor(data)
|
||||||
var ansiState *ansiState
|
} else {
|
||||||
if prevLineAnsiState != nil {
|
transformItem(item, data, nthTransformer, itemIndex)
|
||||||
ansiStateDup := *prevLineAnsiState
|
|
||||||
ansiState = &ansiStateDup
|
|
||||||
}
|
|
||||||
for _, token := range tokens {
|
|
||||||
prevAnsiState := ansiState
|
|
||||||
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
|
|
||||||
if prevAnsiState != nil {
|
|
||||||
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
|
|
||||||
} else {
|
|
||||||
token.text.Prepend("\x1b[m")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
|
||||||
var maxColorOffset int32
|
|
||||||
if item.colors != nil {
|
|
||||||
for _, ansi := range *item.colors {
|
|
||||||
if ansi.color.bg >= 0 {
|
|
||||||
maxColorOffset = max(maxColorOffset, ansi.offset[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
item.origText = &data
|
item.origText = &data
|
||||||
itemIndex++
|
itemIndex++
|
||||||
@@ -193,7 +193,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
|
||||||
var reader *Reader
|
var reader *Reader
|
||||||
if !streamingFilter {
|
if !streamingFilter {
|
||||||
reader = NewReader(func(data []byte) bool {
|
reader = NewReader(func(data []byte) bool {
|
||||||
@@ -236,15 +236,17 @@ func Run(opts *Options) (int, error) {
|
|||||||
denylist = make(map[int32]struct{})
|
denylist = make(map[int32]struct{})
|
||||||
denyMutex.Unlock()
|
denyMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
headerLines := int32(opts.HeaderLines)
|
||||||
|
headerUpdated := false
|
||||||
patternBuilder := func(runes []rune) *Pattern {
|
patternBuilder := func(runes []rune) *Pattern {
|
||||||
denyMutex.Lock()
|
denyMutex.Lock()
|
||||||
denylistCopy := maps.Clone(denylist)
|
denylistCopy := maps.Clone(denylist)
|
||||||
denyMutex.Unlock()
|
denyMutex.Unlock()
|
||||||
return BuildPattern(cache, patternCache,
|
return BuildPattern(cache, patternCache,
|
||||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
|
||||||
}
|
}
|
||||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
if opts.Filter != nil {
|
if opts.Filter != nil {
|
||||||
@@ -265,8 +267,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
func(runes []byte) bool {
|
func(runes []byte) bool {
|
||||||
item := Item{}
|
item := Item{}
|
||||||
if chunkList.trans(&item, runes) {
|
if chunkList.trans(&item, runes) {
|
||||||
|
if item.Index() < headerLines {
|
||||||
|
return false
|
||||||
|
}
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
|
||||||
opts.Printer(transformer(&item))
|
opts.Printer(transformer(&item))
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
@@ -281,6 +286,46 @@ func Run(opts *Options) (int, error) {
|
|||||||
|
|
||||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||||
snapshot, _, _ := chunkList.Snapshot(opts.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{
|
result := matcher.scan(MatchRequest{
|
||||||
chunks: snapshot,
|
chunks: snapshot,
|
||||||
pattern: pattern})
|
pattern: pattern})
|
||||||
@@ -330,10 +375,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
query := []rune{}
|
query := []rune{}
|
||||||
determine := func(final bool) {
|
determine := func(final bool) {
|
||||||
if heightUnknown {
|
if heightUnknown {
|
||||||
if total >= maxFit || final {
|
items := max(0, total-int(headerLines))
|
||||||
|
if items >= maxFit || final {
|
||||||
deferred = false
|
deferred = false
|
||||||
heightUnknown = false
|
heightUnknown = false
|
||||||
terminal.startChan <- fitpad{min(total, maxFit), padHeight}
|
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
|
||||||
}
|
}
|
||||||
} else if deferred {
|
} else if deferred {
|
||||||
deferred = false
|
deferred = false
|
||||||
@@ -349,11 +395,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
clearDenylist()
|
clearDenylist()
|
||||||
}
|
}
|
||||||
reading = true
|
reading = true
|
||||||
|
headerUpdated = false
|
||||||
startTick = ticks
|
startTick = ticks
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
inputRevision.bumpMajor()
|
inputRevision.bumpMajor()
|
||||||
header = make([]string, 0, opts.HeaderLines)
|
|
||||||
readyChan := make(chan bool)
|
readyChan := make(chan bool)
|
||||||
go reader.restart(command, environ, readyChan)
|
go reader.restart(command, environ, readyChan)
|
||||||
<-readyChan
|
<-readyChan
|
||||||
@@ -411,7 +457,11 @@ func Run(opts *Options) (int, error) {
|
|||||||
snapshotRevision = inputRevision
|
snapshotRevision = inputRevision
|
||||||
}
|
}
|
||||||
total = count
|
total = count
|
||||||
terminal.UpdateCount(total, !reading, value.(*string))
|
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
|
||||||
|
if headerLines > 0 && !headerUpdated {
|
||||||
|
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||||
|
headerUpdated = int32(total) >= headerLines
|
||||||
|
}
|
||||||
if heightUnknown && !deferred {
|
if heightUnknown && !deferred {
|
||||||
determine(!reading)
|
determine(!reading)
|
||||||
}
|
}
|
||||||
@@ -421,6 +471,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
var command *commandSpec
|
var command *commandSpec
|
||||||
var environ []string
|
var environ []string
|
||||||
var changed bool
|
var changed bool
|
||||||
|
headerLinesChanged := false
|
||||||
|
withNthChanged := false
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case searchRequest:
|
case searchRequest:
|
||||||
sort = val.sort
|
sort = val.sort
|
||||||
@@ -441,6 +493,40 @@ func Run(opts *Options) (int, error) {
|
|||||||
nth = *val.nth
|
nth = *val.nth
|
||||||
bump = true
|
bump = true
|
||||||
}
|
}
|
||||||
|
if val.headerLines != nil {
|
||||||
|
headerLines = int32(*val.headerLines)
|
||||||
|
headerUpdated = false
|
||||||
|
headerLinesChanged = true
|
||||||
|
bump = true
|
||||||
|
}
|
||||||
|
if val.withNth != nil {
|
||||||
|
newTransformer := val.withNth.fn
|
||||||
|
// Cancel any in-flight scan and block the terminal from reading
|
||||||
|
// items before mutating them in-place. Snapshot shares middle
|
||||||
|
// chunk pointers, so the matcher and terminal can race with us.
|
||||||
|
matcher.CancelScan()
|
||||||
|
terminal.PauseRendering()
|
||||||
|
// Reset cross-line ANSI state before re-processing all items
|
||||||
|
lineAnsiState = nil
|
||||||
|
prevLineAnsiState = nil
|
||||||
|
chunkList.ForEachItem(func(item *Item) {
|
||||||
|
origBytes := *item.origText
|
||||||
|
savedIndex := item.Index()
|
||||||
|
if newTransformer != nil {
|
||||||
|
transformItem(item, origBytes, newTransformer, savedIndex)
|
||||||
|
} else {
|
||||||
|
item.text, item.colors = ansiProcessor(origBytes)
|
||||||
|
}
|
||||||
|
item.text.Index = savedIndex
|
||||||
|
item.transformed = nil
|
||||||
|
}, func() {
|
||||||
|
nthTransformer = newTransformer
|
||||||
|
})
|
||||||
|
terminal.ResumeRendering()
|
||||||
|
matcher.ResumeScan()
|
||||||
|
withNthChanged = true
|
||||||
|
bump = true
|
||||||
|
}
|
||||||
if bump {
|
if bump {
|
||||||
patternCache = make(map[string]*Pattern)
|
patternCache = make(map[string]*Pattern)
|
||||||
cache.Clear()
|
cache.Clear()
|
||||||
@@ -477,6 +563,16 @@ func Run(opts *Options) (int, error) {
|
|||||||
snapshotRevision = inputRevision
|
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)
|
||||||
|
}
|
||||||
|
} else if withNthChanged && headerLines > 0 {
|
||||||
|
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||||
|
}
|
||||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||||
delay = false
|
delay = false
|
||||||
|
|
||||||
@@ -486,11 +582,6 @@ func Run(opts *Options) (int, error) {
|
|||||||
terminal.UpdateProgress(val)
|
terminal.UpdateProgress(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
case EvtHeader:
|
|
||||||
headerPadded := make([]string, opts.HeaderLines)
|
|
||||||
copy(headerPadded, value.([]string))
|
|
||||||
terminal.UpdateHeader(headerPadded)
|
|
||||||
|
|
||||||
case EvtSearchFin:
|
case EvtSearchFin:
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case MatchResult:
|
case MatchResult:
|
||||||
|
|||||||
+29
-10
@@ -3,7 +3,6 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,8 +42,11 @@ type Matcher struct {
|
|||||||
reqBox *util.EventBox
|
reqBox *util.EventBox
|
||||||
partitions int
|
partitions int
|
||||||
slab []*util.Slab
|
slab []*util.Slab
|
||||||
|
sortBuf [][]Result
|
||||||
mergerCache map[string]MatchResult
|
mergerCache map[string]MatchResult
|
||||||
revision revision
|
revision revision
|
||||||
|
scanMutex sync.Mutex
|
||||||
|
cancelScan *util.AtomicBool
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -54,8 +56,11 @@ const (
|
|||||||
|
|
||||||
// NewMatcher returns a new Matcher
|
// NewMatcher returns a new Matcher
|
||||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
|
||||||
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||||
|
if threads > 0 {
|
||||||
|
partitions = threads
|
||||||
|
}
|
||||||
return &Matcher{
|
return &Matcher{
|
||||||
cache: cache,
|
cache: cache,
|
||||||
patternBuilder: patternBuilder,
|
patternBuilder: patternBuilder,
|
||||||
@@ -65,8 +70,10 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
|||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
partitions: partitions,
|
partitions: partitions,
|
||||||
slab: make([]*util.Slab, partitions),
|
slab: make([]*util.Slab, partitions),
|
||||||
|
sortBuf: make([][]Result, partitions),
|
||||||
mergerCache: make(map[string]MatchResult),
|
mergerCache: make(map[string]MatchResult),
|
||||||
revision: revision}
|
revision: revision,
|
||||||
|
cancelScan: util.NewAtomicBool(false)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop puts Matcher in action
|
// Loop puts Matcher in action
|
||||||
@@ -126,7 +133,9 @@ func (m *Matcher) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if result.merger == nil {
|
if result.merger == nil {
|
||||||
|
m.scanMutex.Lock()
|
||||||
result = m.scan(request)
|
result = m.scan(request)
|
||||||
|
m.scanMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result.cancelled {
|
if !result.cancelled {
|
||||||
@@ -174,7 +183,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
|||||||
return MatchResult{m, m, false}
|
return MatchResult{m, m, false}
|
||||||
}
|
}
|
||||||
pattern := request.pattern
|
pattern := request.pattern
|
||||||
passMerger := PassMerger(&request.chunks, m.tac, request.revision)
|
passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
|
||||||
if pattern.IsEmpty() {
|
if pattern.IsEmpty() {
|
||||||
return MatchResult{passMerger, passMerger, false}
|
return MatchResult{passMerger, passMerger, false}
|
||||||
}
|
}
|
||||||
@@ -212,11 +221,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
|||||||
sliceMatches = append(sliceMatches, matches...)
|
sliceMatches = append(sliceMatches, matches...)
|
||||||
}
|
}
|
||||||
if m.sort && request.pattern.sortable {
|
if m.sort && request.pattern.sortable {
|
||||||
if m.tac {
|
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
|
||||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
|
||||||
} else {
|
|
||||||
sort.Sort(ByRelevance(sliceMatches))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resultChan <- partialResult{idx, sliceMatches}
|
resultChan <- partialResult{idx, sliceMatches}
|
||||||
}(idx, m.slab[idx], chunks)
|
}(idx, m.slab[idx], chunks)
|
||||||
@@ -238,7 +243,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.reqBox.Peek(reqReset) {
|
if m.cancelScan.Get() || m.reqBox.Peek(reqReset) {
|
||||||
return MatchResult{nil, nil, wait()}
|
return MatchResult{nil, nil, wait()}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +274,20 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
|||||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelScan cancels any in-flight scan, waits for it to finish,
|
||||||
|
// and prevents new scans from starting until ResumeScan is called.
|
||||||
|
// This is used to safely mutate shared items (e.g., during with-nth changes).
|
||||||
|
func (m *Matcher) CancelScan() {
|
||||||
|
m.cancelScan.Set(true)
|
||||||
|
m.scanMutex.Lock()
|
||||||
|
m.cancelScan.Set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeScan allows scans to proceed again after CancelScan.
|
||||||
|
func (m *Matcher) ResumeScan() {
|
||||||
|
m.scanMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Matcher) Stop() {
|
func (m *Matcher) Stop() {
|
||||||
m.reqBox.Set(reqQuit, nil)
|
m.reqBox.Set(reqQuit, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-23
@@ -10,42 +10,46 @@ func EmptyMerger(revision revision) *Merger {
|
|||||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||||
// a single, globally-sorted list
|
// a single, globally-sorted list
|
||||||
type Merger struct {
|
type Merger struct {
|
||||||
pattern *Pattern
|
pattern *Pattern
|
||||||
lists [][]Result
|
lists [][]Result
|
||||||
merged []Result
|
merged []Result
|
||||||
chunks *[]*Chunk
|
chunks *[]*Chunk
|
||||||
cursors []int
|
cursors []int
|
||||||
sorted bool
|
sorted bool
|
||||||
tac bool
|
tac bool
|
||||||
final bool
|
final bool
|
||||||
count int
|
count int
|
||||||
pass bool
|
pass bool
|
||||||
revision revision
|
startIndex int
|
||||||
minIndex int32
|
revision revision
|
||||||
maxIndex int32
|
minIndex int32
|
||||||
|
maxIndex int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassMerger returns a new Merger that simply returns the items in the
|
// PassMerger returns a new Merger that simply returns the items in the
|
||||||
// original order
|
// original order. startIndex items are skipped from the beginning.
|
||||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
|
||||||
var minIndex, maxIndex int32
|
var minIndex, maxIndex int32
|
||||||
if len(*chunks) > 0 {
|
if len(*chunks) > 0 {
|
||||||
minIndex = (*chunks)[0].items[0].Index()
|
minIndex = (*chunks)[0].items[0].Index()
|
||||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||||
}
|
}
|
||||||
|
si := int(startIndex)
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: nil,
|
pattern: nil,
|
||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
count: 0,
|
count: 0,
|
||||||
pass: true,
|
pass: true,
|
||||||
revision: revision,
|
startIndex: si,
|
||||||
minIndex: minIndex,
|
revision: revision,
|
||||||
maxIndex: maxIndex}
|
minIndex: minIndex + startIndex,
|
||||||
|
maxIndex: maxIndex}
|
||||||
|
|
||||||
for _, chunk := range *mg.chunks {
|
for _, chunk := range *mg.chunks {
|
||||||
mg.count += chunk.count
|
mg.count += chunk.count
|
||||||
}
|
}
|
||||||
|
mg.count = max(0, mg.count-si)
|
||||||
return &mg
|
return &mg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +117,7 @@ func (mg *Merger) Get(idx int) Result {
|
|||||||
if mg.tac {
|
if mg.tac {
|
||||||
idx = mg.count - idx - 1
|
idx = mg.count - idx - 1
|
||||||
}
|
}
|
||||||
|
idx += mg.startIndex
|
||||||
firstChunk := (*mg.chunks)[0]
|
firstChunk := (*mg.chunks)[0]
|
||||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||||
idx -= firstChunk.count
|
idx -= firstChunk.count
|
||||||
|
|||||||
+35
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/algo"
|
"github.com/junegunn/fzf/src/algo"
|
||||||
@@ -587,6 +588,7 @@ type Options struct {
|
|||||||
FreezeLeft int
|
FreezeLeft int
|
||||||
FreezeRight int
|
FreezeRight int
|
||||||
WithNth func(Delimiter) func([]Token, int32) string
|
WithNth func(Delimiter) func([]Token, int32) string
|
||||||
|
WithNthExpr string
|
||||||
AcceptNth func(Delimiter) func([]Token, int32) string
|
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
@@ -677,6 +679,8 @@ type Options struct {
|
|||||||
WalkerSkip []string
|
WalkerSkip []string
|
||||||
Version bool
|
Version bool
|
||||||
Help bool
|
Help bool
|
||||||
|
Threads int
|
||||||
|
Bench time.Duration
|
||||||
CPUProfile string
|
CPUProfile string
|
||||||
MEMProfile string
|
MEMProfile string
|
||||||
BlockProfile string
|
BlockProfile string
|
||||||
@@ -1626,7 +1630,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
executeRegexp = regexp.MustCompile(
|
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|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-lines|header|footer|search|with-nth|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
||||||
splitRegexp = regexp.MustCompile("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
@@ -2037,6 +2041,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actPreview
|
return actPreview
|
||||||
case "change-header":
|
case "change-header":
|
||||||
return actChangeHeader
|
return actChangeHeader
|
||||||
|
case "change-header-lines":
|
||||||
|
return actChangeHeaderLines
|
||||||
case "change-footer":
|
case "change-footer":
|
||||||
return actChangeFooter
|
return actChangeFooter
|
||||||
case "change-list-label":
|
case "change-list-label":
|
||||||
@@ -2067,6 +2073,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actChangeMulti
|
return actChangeMulti
|
||||||
case "change-nth":
|
case "change-nth":
|
||||||
return actChangeNth
|
return actChangeNth
|
||||||
|
case "change-with-nth":
|
||||||
|
return actChangeWithNth
|
||||||
case "pos":
|
case "pos":
|
||||||
return actPosition
|
return actPosition
|
||||||
case "execute":
|
case "execute":
|
||||||
@@ -2097,10 +2105,14 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actTransformFooter
|
return actTransformFooter
|
||||||
case "transform-header":
|
case "transform-header":
|
||||||
return actTransformHeader
|
return actTransformHeader
|
||||||
|
case "transform-header-lines":
|
||||||
|
return actTransformHeaderLines
|
||||||
case "transform-ghost":
|
case "transform-ghost":
|
||||||
return actTransformGhost
|
return actTransformGhost
|
||||||
case "transform-nth":
|
case "transform-nth":
|
||||||
return actTransformNth
|
return actTransformNth
|
||||||
|
case "transform-with-nth":
|
||||||
|
return actTransformWithNth
|
||||||
case "transform-pointer":
|
case "transform-pointer":
|
||||||
return actTransformPointer
|
return actTransformPointer
|
||||||
case "transform-prompt":
|
case "transform-prompt":
|
||||||
@@ -2127,10 +2139,14 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actBgTransformFooter
|
return actBgTransformFooter
|
||||||
case "bg-transform-header":
|
case "bg-transform-header":
|
||||||
return actBgTransformHeader
|
return actBgTransformHeader
|
||||||
|
case "bg-transform-header-lines":
|
||||||
|
return actBgTransformHeaderLines
|
||||||
case "bg-transform-ghost":
|
case "bg-transform-ghost":
|
||||||
return actBgTransformGhost
|
return actBgTransformGhost
|
||||||
case "bg-transform-nth":
|
case "bg-transform-nth":
|
||||||
return actBgTransformNth
|
return actBgTransformNth
|
||||||
|
case "bg-transform-with-nth":
|
||||||
|
return actBgTransformWithNth
|
||||||
case "bg-transform-pointer":
|
case "bg-transform-pointer":
|
||||||
return actBgTransformPointer
|
return actBgTransformPointer
|
||||||
case "bg-transform-prompt":
|
case "bg-transform-prompt":
|
||||||
@@ -2772,6 +2788,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if opts.WithNth, err = nthTransformer(str); err != nil {
|
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
opts.WithNthExpr = str
|
||||||
case "--accept-nth":
|
case "--accept-nth":
|
||||||
str, err := nextString("nth expression required")
|
str, err := nextString("nth expression required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3367,6 +3384,23 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
|
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":
|
case "--profile-cpu":
|
||||||
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
|
if opts.CPUProfile, err = nextString("file path required: cpu"); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+79
-21
@@ -64,6 +64,9 @@ type Pattern struct {
|
|||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
denylist map[int32]struct{}
|
denylist map[int32]struct{}
|
||||||
|
startIndex int32
|
||||||
|
directAlgo algo.Algo
|
||||||
|
directTerm *term
|
||||||
}
|
}
|
||||||
|
|
||||||
var _splitRegex *regexp.Regexp
|
var _splitRegex *regexp.Regexp
|
||||||
@@ -74,7 +77,7 @@ func init() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// 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,
|
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{}) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
|
||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
@@ -146,9 +149,11 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
denylist: denylist,
|
denylist: denylist,
|
||||||
|
startIndex: startIndex,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
ptr.cacheKey = ptr.buildCacheKey()
|
ptr.cacheKey = ptr.buildCacheKey()
|
||||||
|
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
|
||||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||||
ptr.procFun[termEqual] = algo.EqualMatch
|
ptr.procFun[termEqual] = algo.EqualMatch
|
||||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||||
@@ -272,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
|
|||||||
return strings.Join(cacheableTerms, "\t")
|
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
|
// CacheKey is used to build string to be used as the key of result cache
|
||||||
func (p *Pattern) CacheKey() string {
|
func (p *Pattern) CacheKey() string {
|
||||||
return p.cacheKey
|
return p.cacheKey
|
||||||
@@ -301,18 +322,56 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
|||||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
if len(p.denylist) == 0 {
|
// Skip header items in chunks that contain them
|
||||||
// Huge code duplication for minimizing unnecessary map lookups
|
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 {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := startIdx; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||||
matches = append(matches, *match)
|
&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 {
|
} else {
|
||||||
for _, result := range space {
|
for _, result := range space {
|
||||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||||
matches = append(matches, *match)
|
&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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, result := range space {
|
||||||
|
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||||
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,12 +379,12 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := startIdx; idx < chunk.count; idx++ {
|
||||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||||
matches = append(matches, *match)
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -333,30 +392,29 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||||
matches = append(matches, *match)
|
matches = append(matches, match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchItem returns true if the Item is a match
|
// MatchItem returns the match result if the Item is a match.
|
||||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
// A zero-value Result (with item == nil) indicates no match.
|
||||||
|
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
|
||||||
if p.extended {
|
if p.extended {
|
||||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||||
result := buildResult(item, offsets, bonus)
|
return buildResult(item, offsets, bonus), offsets, pos
|
||||||
return &result, offsets, pos
|
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return Result{}, nil, nil
|
||||||
}
|
}
|
||||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||||
if sidx := offset[0]; sidx >= 0 {
|
if sidx := offset[0]; sidx >= 0 {
|
||||||
offsets := []Offset{offset}
|
offsets := []Offset{offset}
|
||||||
result := buildResult(item, offsets, bonus)
|
return buildResult(item, offsets, bonus), offsets, pos
|
||||||
return &result, offsets, pos
|
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return Result{}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
|
|||||||
+93
-8
@@ -33,8 +33,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
sort.Sort(ByOrder(offsets))
|
sort.Sort(ByOrder(offsets))
|
||||||
}
|
}
|
||||||
|
|
||||||
result := Result{item: item}
|
|
||||||
numChars := item.text.Length()
|
|
||||||
minBegin := math.MaxUint16
|
minBegin := math.MaxUint16
|
||||||
minEnd := math.MaxUint16
|
minEnd := math.MaxUint16
|
||||||
maxEnd := 0
|
maxEnd := 0
|
||||||
@@ -49,6 +47,14 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
for idx, criterion := range sortCriteria {
|
||||||
val := uint16(math.MaxUint16)
|
val := uint16(math.MaxUint16)
|
||||||
switch criterion {
|
switch criterion {
|
||||||
@@ -75,7 +81,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
val = item.TrimLength()
|
val = item.TrimLength()
|
||||||
case byPathname:
|
case byPathname:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
|
||||||
lastDelim := -1
|
lastDelim := -1
|
||||||
s := item.text.ToString()
|
s := item.text.ToString()
|
||||||
for i := len(s) - 1; i >= 0; i-- {
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
@@ -123,7 +128,7 @@ func minRank() Result {
|
|||||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
|
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, nthOverlay tui.Attr, hidden bool) []colorOffset {
|
||||||
itemColors := result.item.Colors()
|
itemColors := result.item.Colors()
|
||||||
|
|
||||||
// No ANSI codes
|
// No ANSI codes
|
||||||
@@ -208,6 +213,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
}
|
}
|
||||||
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
|
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
|
||||||
}
|
}
|
||||||
|
fgAttr := tui.ColNormal.Attr()
|
||||||
|
nthAttrFinal := fgAttr.Merge(attrNth).Merge(nthOverlay)
|
||||||
|
nthBase := colBase.WithNewAttr(nthAttrFinal)
|
||||||
|
|
||||||
var colors []colorOffset
|
var colors []colorOffset
|
||||||
add := func(idx int) {
|
add := func(idx int) {
|
||||||
if curr.fbg >= 0 {
|
if curr.fbg >= 0 {
|
||||||
@@ -221,7 +230,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
if curr.match {
|
if curr.match {
|
||||||
var color tui.ColorPair
|
var color tui.ColorPair
|
||||||
if curr.nth {
|
if curr.nth {
|
||||||
color = colBase.WithAttr(attrNth).Merge(colMatch)
|
color = nthBase.Merge(colMatch)
|
||||||
} else {
|
} else {
|
||||||
color = colBase.Merge(colMatch)
|
color = colBase.Merge(colMatch)
|
||||||
}
|
}
|
||||||
@@ -241,7 +250,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||||
color = origColor
|
color = origColor
|
||||||
if curr.nth {
|
if curr.nth {
|
||||||
color = color.WithAttr(attrNth &^ tui.AttrRegular)
|
color = color.WithAttr((attrNth &^ tui.AttrRegular).Merge(nthOverlay))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
color = origColor.MergeNonDefault(color)
|
color = origColor.MergeNonDefault(color)
|
||||||
@@ -253,7 +262,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
ansi := itemColors[curr.index]
|
ansi := itemColors[curr.index]
|
||||||
base := colBase
|
base := colBase
|
||||||
if curr.nth {
|
if curr.nth {
|
||||||
base = base.WithAttr(attrNth)
|
base = nthBase
|
||||||
}
|
}
|
||||||
if hidden {
|
if hidden {
|
||||||
base = base.WithFg(theme.Nomatch)
|
base = base.WithFg(theme.Nomatch)
|
||||||
@@ -265,7 +274,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
|||||||
match: false,
|
match: false,
|
||||||
url: ansi.color.url})
|
url: ansi.color.url})
|
||||||
} else {
|
} else {
|
||||||
color := colBase.WithAttr(attrNth)
|
color := nthBase
|
||||||
if hidden {
|
if hidden {
|
||||||
color = color.WithFg(theme.Nomatch)
|
color = color.WithFg(theme.Nomatch)
|
||||||
}
|
}
|
||||||
@@ -334,3 +343,79 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
|||||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||||
return compareRanks(a[i], a[j], true)
|
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,4 +1,4 @@
|
|||||||
//go:build !386 && !amd64
|
//go:build !386 && !amd64 && !arm64
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
|||||||
}
|
}
|
||||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
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
|
||||||
|
}
|
||||||
|
|||||||
+91
-2
@@ -2,6 +2,7 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ func TestColorOffset(t *testing.T) {
|
|||||||
|
|
||||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
|
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, 0, false)
|
||||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||||
o := colors[idx]
|
o := colors[idx]
|
||||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||||
@@ -158,7 +159,7 @@ func TestColorOffset(t *testing.T) {
|
|||||||
|
|
||||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
|
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, 0, false)
|
||||||
|
|
||||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||||
@@ -181,4 +182,92 @@ func TestColorOffset(t *testing.T) {
|
|||||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test nthOverlay: simulates nth:regular with current-fg:underline
|
||||||
|
// The overlay (underline) should survive even though nth:regular clears attrs.
|
||||||
|
// Precedence: fg < nth < current-fg
|
||||||
|
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.AttrRegular, tui.Underline, false)
|
||||||
|
|
||||||
|
// nth regions should have Underline (from overlay), not cleared by AttrRegular
|
||||||
|
// Non-nth regions keep colBase attrs (AttrUndefined)
|
||||||
|
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
|
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||||
|
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
|
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||||
|
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||||
|
assert(5, 27, 30, colUnderline)
|
||||||
|
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||||
|
assert(7, 32, 33, colUnderline)
|
||||||
|
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||||
|
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
|
// nth region within ANSI bold: AttrRegular clears, overlay adds Underline back
|
||||||
|
assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||||
|
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
|
|
||||||
|
// Test nthOverlay with additive attrs: nth:strikethrough with selected-fg:bold
|
||||||
|
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.StrikeThrough, tui.Bold, false)
|
||||||
|
|
||||||
|
// Non-nth entries unchanged from overlay=0 case
|
||||||
|
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
|
assert(5, 27, 30, colUnderline) // match only, no nth
|
||||||
|
assert(7, 32, 33, colUnderline) // match only, no nth
|
||||||
|
// nth region within ANSI bold: StrikeThrough|Bold merged with ANSI Bold
|
||||||
|
assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.StrikeThrough))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
//go:build 386 || amd64
|
//go:build 386 || amd64 || arm64
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
|||||||
}
|
}
|
||||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sortKey(r *Result) uint64 {
|
||||||
|
return *(*uint64)(unsafe.Pointer(&r.points[0]))
|
||||||
|
}
|
||||||
|
|||||||
+170
-33
@@ -314,6 +314,7 @@ type Terminal struct {
|
|||||||
sort bool
|
sort bool
|
||||||
toggleSort bool
|
toggleSort bool
|
||||||
track trackOption
|
track trackOption
|
||||||
|
targetIndex int32
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
expect map[tui.Event]string
|
expect map[tui.Event]string
|
||||||
keymap map[tui.Event][]*action
|
keymap map[tui.Event][]*action
|
||||||
@@ -327,7 +328,7 @@ type Terminal struct {
|
|||||||
headerVisible bool
|
headerVisible bool
|
||||||
headerFirst bool
|
headerFirst bool
|
||||||
headerLines int
|
headerLines int
|
||||||
header []string
|
header []Item
|
||||||
header0 []string
|
header0 []string
|
||||||
footer []string
|
footer []string
|
||||||
ellipsis string
|
ellipsis string
|
||||||
@@ -339,6 +340,9 @@ type Terminal struct {
|
|||||||
nthAttr tui.Attr
|
nthAttr tui.Attr
|
||||||
nth []Range
|
nth []Range
|
||||||
nthCurrent []Range
|
nthCurrent []Range
|
||||||
|
withNthDefault string
|
||||||
|
withNthExpr string
|
||||||
|
withNthEnabled bool
|
||||||
acceptNth func([]Token, int32) string
|
acceptNth func([]Token, int32) string
|
||||||
tabstop int
|
tabstop int
|
||||||
margin [4]sizeSpec
|
margin [4]sizeSpec
|
||||||
@@ -383,6 +387,7 @@ type Terminal struct {
|
|||||||
hasLoadActions bool
|
hasLoadActions bool
|
||||||
hasResizeActions bool
|
hasResizeActions bool
|
||||||
triggerLoad bool
|
triggerLoad bool
|
||||||
|
filterSelection bool
|
||||||
reading bool
|
reading bool
|
||||||
running *util.AtomicBool
|
running *util.AtomicBool
|
||||||
failed *string
|
failed *string
|
||||||
@@ -542,6 +547,7 @@ const (
|
|||||||
actChangeBorderLabel
|
actChangeBorderLabel
|
||||||
actChangeGhost
|
actChangeGhost
|
||||||
actChangeHeader
|
actChangeHeader
|
||||||
|
actChangeHeaderLines
|
||||||
actChangeFooter
|
actChangeFooter
|
||||||
actChangeHeaderLabel
|
actChangeHeaderLabel
|
||||||
actChangeFooterLabel
|
actChangeFooterLabel
|
||||||
@@ -549,6 +555,7 @@ const (
|
|||||||
actChangeListLabel
|
actChangeListLabel
|
||||||
actChangeMulti
|
actChangeMulti
|
||||||
actChangeNth
|
actChangeNth
|
||||||
|
actChangeWithNth
|
||||||
actChangePointer
|
actChangePointer
|
||||||
actChangePreview
|
actChangePreview
|
||||||
actChangePreviewLabel
|
actChangePreviewLabel
|
||||||
@@ -627,12 +634,14 @@ const (
|
|||||||
actTransformBorderLabel
|
actTransformBorderLabel
|
||||||
actTransformGhost
|
actTransformGhost
|
||||||
actTransformHeader
|
actTransformHeader
|
||||||
|
actTransformHeaderLines
|
||||||
actTransformFooter
|
actTransformFooter
|
||||||
actTransformHeaderLabel
|
actTransformHeaderLabel
|
||||||
actTransformFooterLabel
|
actTransformFooterLabel
|
||||||
actTransformInputLabel
|
actTransformInputLabel
|
||||||
actTransformListLabel
|
actTransformListLabel
|
||||||
actTransformNth
|
actTransformNth
|
||||||
|
actTransformWithNth
|
||||||
actTransformPointer
|
actTransformPointer
|
||||||
actTransformPreviewLabel
|
actTransformPreviewLabel
|
||||||
actTransformPrompt
|
actTransformPrompt
|
||||||
@@ -645,12 +654,14 @@ const (
|
|||||||
actBgTransformBorderLabel
|
actBgTransformBorderLabel
|
||||||
actBgTransformGhost
|
actBgTransformGhost
|
||||||
actBgTransformHeader
|
actBgTransformHeader
|
||||||
|
actBgTransformHeaderLines
|
||||||
actBgTransformFooter
|
actBgTransformFooter
|
||||||
actBgTransformHeaderLabel
|
actBgTransformHeaderLabel
|
||||||
actBgTransformFooterLabel
|
actBgTransformFooterLabel
|
||||||
actBgTransformInputLabel
|
actBgTransformInputLabel
|
||||||
actBgTransformListLabel
|
actBgTransformListLabel
|
||||||
actBgTransformNth
|
actBgTransformNth
|
||||||
|
actBgTransformWithNth
|
||||||
actBgTransformPointer
|
actBgTransformPointer
|
||||||
actBgTransformPreviewLabel
|
actBgTransformPreviewLabel
|
||||||
actBgTransformPrompt
|
actBgTransformPrompt
|
||||||
@@ -710,12 +721,14 @@ func processExecution(action actionType) bool {
|
|||||||
actTransformBorderLabel,
|
actTransformBorderLabel,
|
||||||
actTransformGhost,
|
actTransformGhost,
|
||||||
actTransformHeader,
|
actTransformHeader,
|
||||||
|
actTransformHeaderLines,
|
||||||
actTransformFooter,
|
actTransformFooter,
|
||||||
actTransformHeaderLabel,
|
actTransformHeaderLabel,
|
||||||
actTransformFooterLabel,
|
actTransformFooterLabel,
|
||||||
actTransformInputLabel,
|
actTransformInputLabel,
|
||||||
actTransformListLabel,
|
actTransformListLabel,
|
||||||
actTransformNth,
|
actTransformNth,
|
||||||
|
actTransformWithNth,
|
||||||
actTransformPointer,
|
actTransformPointer,
|
||||||
actTransformPreviewLabel,
|
actTransformPreviewLabel,
|
||||||
actTransformPrompt,
|
actTransformPrompt,
|
||||||
@@ -725,12 +738,14 @@ func processExecution(action actionType) bool {
|
|||||||
actBgTransformBorderLabel,
|
actBgTransformBorderLabel,
|
||||||
actBgTransformGhost,
|
actBgTransformGhost,
|
||||||
actBgTransformHeader,
|
actBgTransformHeader,
|
||||||
|
actBgTransformHeaderLines,
|
||||||
actBgTransformFooter,
|
actBgTransformFooter,
|
||||||
actBgTransformHeaderLabel,
|
actBgTransformHeaderLabel,
|
||||||
actBgTransformFooterLabel,
|
actBgTransformFooterLabel,
|
||||||
actBgTransformInputLabel,
|
actBgTransformInputLabel,
|
||||||
actBgTransformListLabel,
|
actBgTransformListLabel,
|
||||||
actBgTransformNth,
|
actBgTransformNth,
|
||||||
|
actBgTransformWithNth,
|
||||||
actBgTransformPointer,
|
actBgTransformPointer,
|
||||||
actBgTransformPreviewLabel,
|
actBgTransformPreviewLabel,
|
||||||
actBgTransformPrompt,
|
actBgTransformPrompt,
|
||||||
@@ -760,15 +775,21 @@ type placeholderFlags struct {
|
|||||||
raw bool
|
raw bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type withNthSpec struct {
|
||||||
|
fn func([]Token, int32) string // nil = clear (restore original)
|
||||||
|
}
|
||||||
|
|
||||||
type searchRequest struct {
|
type searchRequest struct {
|
||||||
sort bool
|
sort bool
|
||||||
sync bool
|
sync bool
|
||||||
nth *[]Range
|
nth *[]Range
|
||||||
command *commandSpec
|
withNth *withNthSpec
|
||||||
environ []string
|
headerLines *int
|
||||||
changed bool
|
command *commandSpec
|
||||||
denylist []int32
|
environ []string
|
||||||
revision revision
|
changed bool
|
||||||
|
denylist []int32
|
||||||
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
type previewRequest struct {
|
type previewRequest struct {
|
||||||
@@ -1022,6 +1043,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
sort: opts.Sort > 0,
|
sort: opts.Sort > 0,
|
||||||
toggleSort: opts.ToggleSort,
|
toggleSort: opts.ToggleSort,
|
||||||
track: opts.Track,
|
track: opts.Track,
|
||||||
|
targetIndex: minItem.Index(),
|
||||||
delimiter: opts.Delimiter,
|
delimiter: opts.Delimiter,
|
||||||
expect: opts.Expect,
|
expect: opts.Expect,
|
||||||
keymap: opts.Keymap,
|
keymap: opts.Keymap,
|
||||||
@@ -1063,7 +1085,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
headerFirst: opts.HeaderFirst,
|
headerFirst: opts.HeaderFirst,
|
||||||
headerLines: opts.HeaderLines,
|
headerLines: opts.HeaderLines,
|
||||||
gap: opts.Gap,
|
gap: opts.Gap,
|
||||||
header: []string{},
|
header: []Item{},
|
||||||
footer: opts.Footer,
|
footer: opts.Footer,
|
||||||
header0: opts.Header,
|
header0: opts.Header,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
@@ -1072,6 +1094,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
nthAttr: opts.Theme.Nth.Attr,
|
nthAttr: opts.Theme.Nth.Attr,
|
||||||
nth: opts.Nth,
|
nth: opts.Nth,
|
||||||
nthCurrent: opts.Nth,
|
nthCurrent: opts.Nth,
|
||||||
|
withNthDefault: opts.WithNthExpr,
|
||||||
|
withNthExpr: opts.WithNthExpr,
|
||||||
|
withNthEnabled: opts.WithNth != nil,
|
||||||
tabstop: opts.Tabstop,
|
tabstop: opts.Tabstop,
|
||||||
raw: opts.Raw,
|
raw: opts.Raw,
|
||||||
hasStartActions: false,
|
hasStartActions: false,
|
||||||
@@ -1343,6 +1368,9 @@ func (t *Terminal) environImpl(forPreview bool) []string {
|
|||||||
if len(t.nthCurrent) > 0 {
|
if len(t.nthCurrent) > 0 {
|
||||||
env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
|
env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
|
||||||
}
|
}
|
||||||
|
if len(t.withNthExpr) > 0 {
|
||||||
|
env = append(env, "FZF_WITH_NTH="+t.withNthExpr)
|
||||||
|
}
|
||||||
if t.raw {
|
if t.raw {
|
||||||
val := "0"
|
val := "0"
|
||||||
if t.isCurrentItemMatch() {
|
if t.isCurrentItemMatch() {
|
||||||
@@ -1526,7 +1554,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
|
|||||||
printFn := func(window tui.Window, limit int) {
|
printFn := func(window tui.Window, limit int) {
|
||||||
if offsets == nil {
|
if offsets == nil {
|
||||||
// tui.Col* are not initialized until renderer.Init()
|
// tui.Col* are not initialized until renderer.Init()
|
||||||
offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, false)
|
offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, 0, false)
|
||||||
}
|
}
|
||||||
for limit > 0 {
|
for limit > 0 {
|
||||||
if length > limit {
|
if length > limit {
|
||||||
@@ -1589,7 +1617,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
t.printHighlighted(
|
t.printHighlighted(
|
||||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil)
|
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil, 0)
|
||||||
})
|
})
|
||||||
t.wrap = wrap
|
t.wrap = wrap
|
||||||
}
|
}
|
||||||
@@ -1712,6 +1740,17 @@ func (t *Terminal) Input() (bool, []rune) {
|
|||||||
return paused, copySlice(src)
|
return paused, copySlice(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PauseRendering blocks the terminal from reading items.
|
||||||
|
// Must be paired with ResumeRendering.
|
||||||
|
func (t *Terminal) PauseRendering() {
|
||||||
|
t.mutex.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeRendering releases the lock acquired by PauseRendering.
|
||||||
|
func (t *Terminal) ResumeRendering() {
|
||||||
|
t.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCount updates the count information
|
// UpdateCount updates the count information
|
||||||
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
|
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
@@ -1755,8 +1794,14 @@ func (t *Terminal) changeFooter(footer string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHeader updates the header
|
// UpdateHeader updates the header
|
||||||
func (t *Terminal) UpdateHeader(header []string) {
|
func (t *Terminal) UpdateHeader(header []Item) {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
|
// Pad to t.headerLines so that click coordinate mapping works correctly
|
||||||
|
if len(header) < t.headerLines {
|
||||||
|
padded := make([]Item, t.headerLines)
|
||||||
|
copy(padded, header)
|
||||||
|
header = padded
|
||||||
|
}
|
||||||
t.header = header
|
t.header = header
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
t.reqBox.Set(reqHeader, nil)
|
t.reqBox.Set(reqHeader, nil)
|
||||||
@@ -1788,6 +1833,10 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
prevIndex = merger.First().item.Index()
|
prevIndex = merger.First().item.Index()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.targetIndex != minItem.Index() {
|
||||||
|
prevIndex = t.targetIndex
|
||||||
|
t.targetIndex = minItem.Index()
|
||||||
|
}
|
||||||
t.progress = 100
|
t.progress = 100
|
||||||
t.merger = merger
|
t.merger = merger
|
||||||
t.resultMerger = merger
|
t.resultMerger = merger
|
||||||
@@ -1824,6 +1873,21 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
|||||||
}
|
}
|
||||||
t.revision = newRevision
|
t.revision = newRevision
|
||||||
t.version++
|
t.version++
|
||||||
|
|
||||||
|
// Filter out selections that no longer match after with-nth change.
|
||||||
|
// Must be inside the revision check so we don't consume the flag
|
||||||
|
// on a stale EvtSearchFin from a previous search.
|
||||||
|
if t.filterSelection && t.multi > 0 && len(t.selected) > 0 {
|
||||||
|
matchMap := t.resultMerger.ToMap()
|
||||||
|
filtered := make(map[int32]selectedItem)
|
||||||
|
for k, v := range t.selected {
|
||||||
|
if _, matched := matchMap[k]; matched {
|
||||||
|
filtered[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.selected = filtered
|
||||||
|
}
|
||||||
|
t.filterSelection = false
|
||||||
}
|
}
|
||||||
if t.triggerLoad {
|
if t.triggerLoad {
|
||||||
t.triggerLoad = false
|
t.triggerLoad = false
|
||||||
@@ -3079,11 +3143,11 @@ func (t *Terminal) printHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.withWindow(t.headerWindow, func() {
|
t.withWindow(t.headerWindow, func() {
|
||||||
var lines []string
|
var headerItems []Item
|
||||||
if !t.hasHeaderLinesWindow() {
|
if !t.hasHeaderLinesWindow() {
|
||||||
lines = t.header
|
headerItems = t.header
|
||||||
}
|
}
|
||||||
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, lines)
|
t.printHeaderImpl(t.headerWindow, t.headerBorderShape, t.header0, headerItems)
|
||||||
})
|
})
|
||||||
if w, shape := t.determineHeaderLinesShape(); w {
|
if w, shape := t.determineHeaderLinesShape(); w {
|
||||||
t.withWindow(t.headerLinesWindow, func() {
|
t.withWindow(t.headerLinesWindow, func() {
|
||||||
@@ -3121,7 +3185,7 @@ func (t *Terminal) printFooter() {
|
|||||||
func(markerClass) int {
|
func(markerClass) int {
|
||||||
t.footerWindow.Print(indent)
|
t.footerWindow.Print(indent)
|
||||||
return indentSize
|
return indentSize
|
||||||
}, nil)
|
}, nil, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.wrap = wrap
|
t.wrap = wrap
|
||||||
@@ -3145,7 +3209,7 @@ func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
|
|||||||
return indentSize
|
return indentSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []string) {
|
func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShape, lines1 []string, lines2 []Item) {
|
||||||
max := t.window.Height()
|
max := t.window.Height()
|
||||||
if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst {
|
if !t.inputless && t.inputWindow == nil && window == nil && t.headerFirst {
|
||||||
max--
|
max--
|
||||||
@@ -3172,7 +3236,8 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
|
|||||||
}
|
}
|
||||||
indent := strings.Repeat(" ", indentSize)
|
indent := strings.Repeat(" ", indentSize)
|
||||||
t.wrap = false
|
t.wrap = false
|
||||||
for idx, lineStr := range append(append([]string{}, lines1...), lines2...) {
|
totalLines := len(lines1) + len(lines2)
|
||||||
|
for idx := 0; idx < totalLines; idx++ {
|
||||||
line := idx
|
line := idx
|
||||||
if needReverse && idx < len(lines1) {
|
if needReverse && idx < len(lines1) {
|
||||||
line = len(lines1) - idx - 1
|
line = len(lines1) - idx - 1
|
||||||
@@ -3186,18 +3251,25 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
|
|||||||
if line >= max {
|
if line >= max {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
trimmed, colors, newState := extractColor(lineStr, state, nil)
|
|
||||||
state = newState
|
var item *Item
|
||||||
item := &Item{
|
if idx < len(lines1) {
|
||||||
text: util.ToChars([]byte(trimmed)),
|
trimmed, colors, newState := extractColor(lines1[idx], state, nil)
|
||||||
colors: colors}
|
state = newState
|
||||||
|
item = &Item{
|
||||||
|
text: util.ToChars([]byte(trimmed)),
|
||||||
|
colors: colors}
|
||||||
|
} else {
|
||||||
|
headerItem := lines2[idx-len(lines1)]
|
||||||
|
item = &headerItem
|
||||||
|
}
|
||||||
|
|
||||||
t.printHighlighted(Result{item: item},
|
t.printHighlighted(Result{item: item},
|
||||||
tui.ColHeader, tui.ColHeader, false, false, false, line, line, true,
|
tui.ColHeader, tui.ColHeader, false, false, false, line, line, true,
|
||||||
func(markerClass) int {
|
func(markerClass) int {
|
||||||
t.window.Print(indent)
|
t.window.Print(indent)
|
||||||
return indentSize
|
return indentSize
|
||||||
}, nil)
|
}, nil, 0)
|
||||||
}
|
}
|
||||||
t.wrap = wrap
|
t.wrap = wrap
|
||||||
}
|
}
|
||||||
@@ -3435,7 +3507,14 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
|||||||
}
|
}
|
||||||
return indentSize
|
return indentSize
|
||||||
}
|
}
|
||||||
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
|
colCurrent := tui.ColCurrent
|
||||||
|
nthOverlay := t.theme.NthCurrentAttr
|
||||||
|
if selected {
|
||||||
|
nthOverlay = t.theme.NthSelectedAttr.Merge(t.theme.NthCurrentAttr)
|
||||||
|
baseAttr := tui.ColNormal.Attr().Merge(t.theme.NthSelectedAttr).Merge(t.theme.NthCurrentAttr)
|
||||||
|
colCurrent = colCurrent.WithNewAttr(baseAttr)
|
||||||
|
}
|
||||||
|
finalLineNum = t.printHighlighted(result, colCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
|
||||||
} else {
|
} else {
|
||||||
preTask := func(marker markerClass) int {
|
preTask := func(marker markerClass) int {
|
||||||
w := t.window.Width() - t.pointerLen
|
w := t.window.Width() - t.pointerLen
|
||||||
@@ -3469,7 +3548,11 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
|||||||
base = base.WithBg(altBg)
|
base = base.WithBg(altBg)
|
||||||
match = match.WithBg(altBg)
|
match = match.WithBg(altBg)
|
||||||
}
|
}
|
||||||
finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
|
var nthOverlay tui.Attr
|
||||||
|
if selected {
|
||||||
|
nthOverlay = t.theme.NthSelectedAttr
|
||||||
|
}
|
||||||
|
finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask, nthOverlay)
|
||||||
}
|
}
|
||||||
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
|
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
|
||||||
finalLineNum++
|
finalLineNum++
|
||||||
@@ -3570,7 +3653,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
|
|||||||
return t.displayWidthWithLimit(runes, 0, max) > max
|
return t.displayWidthWithLimit(runes, 0, max) > max
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, hidden bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair)) int {
|
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, hidden bool, lineNum int, maxLineNum int, forceRedraw bool, preTask func(markerClass) int, postTask func(int, int, bool, bool, tui.ColorPair), nthOverlay tui.Attr) int {
|
||||||
var displayWidth int
|
var displayWidth int
|
||||||
item := result.item
|
item := result.item
|
||||||
matchOffsets := []Offset{}
|
matchOffsets := []Offset{}
|
||||||
@@ -3611,7 +3694,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
// But if 'nth' is set to 'regular', it's a sign that you're applying
|
// But if 'nth' is set to 'regular', it's a sign that you're applying
|
||||||
// a different style to the rest of the string. e.g. 'nth:regular,fg:dim'
|
// a different style to the rest of the string. e.g. 'nth:regular,fg:dim'
|
||||||
// In this case, we still need to apply it to clear the style.
|
// In this case, we still need to apply it to clear the style.
|
||||||
colBase = colBase.WithAttr(t.nthAttr)
|
fgAttr := tui.ColNormal.Attr()
|
||||||
|
nthAttrFinal := fgAttr.Merge(t.nthAttr).Merge(nthOverlay)
|
||||||
|
colBase = colBase.WithNewAttr(nthAttrFinal)
|
||||||
}
|
}
|
||||||
if !wholeCovered && t.nthAttr > 0 {
|
if !wholeCovered && t.nthAttr > 0 {
|
||||||
var tokens []Token
|
var tokens []Token
|
||||||
@@ -3630,7 +3715,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
sort.Sort(ByOrder(nthOffsets))
|
sort.Sort(ByOrder(nthOffsets))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, hidden)
|
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
|
||||||
|
|
||||||
// Determine split offset for horizontal scrolling with freeze
|
// Determine split offset for horizontal scrolling with freeze
|
||||||
splitOffset1 := -1
|
splitOffset1 := -1
|
||||||
@@ -5288,9 +5373,13 @@ func (t *Terminal) addClickHeaderWord(env []string) []string {
|
|||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: t.header is padded with empty strings so that its size is equal to t.headerLines
|
|
||||||
nthBase := 0
|
nthBase := 0
|
||||||
headers := [2][]string{t.header, t.header0}
|
// Convert header items to strings for click handling
|
||||||
|
headerStrs := make([]string, len(t.header))
|
||||||
|
for i, item := range t.header {
|
||||||
|
headerStrs[i] = item.text.ToString()
|
||||||
|
}
|
||||||
|
headers := [2][]string{headerStrs, t.header0}
|
||||||
if t.layout == layoutReverse {
|
if t.layout == layoutReverse {
|
||||||
headers[0], headers[1] = headers[1], headers[0]
|
headers[0], headers[1] = headers[1], headers[0]
|
||||||
}
|
}
|
||||||
@@ -5892,6 +5981,8 @@ func (t *Terminal) Loop() error {
|
|||||||
events := []util.EventType{}
|
events := []util.EventType{}
|
||||||
changed := false
|
changed := false
|
||||||
var newNth *[]Range
|
var newNth *[]Range
|
||||||
|
var newWithNth *withNthSpec
|
||||||
|
var newHeaderLines *int
|
||||||
req := func(evts ...util.EventType) {
|
req := func(evts ...util.EventType) {
|
||||||
for _, event := range evts {
|
for _, event := range evts {
|
||||||
events = append(events, event)
|
events = append(events, event)
|
||||||
@@ -5908,6 +5999,8 @@ func (t *Terminal) Loop() error {
|
|||||||
events = []util.EventType{}
|
events = []util.EventType{}
|
||||||
changed = false
|
changed = false
|
||||||
newNth = nil
|
newNth = nil
|
||||||
|
newWithNth = nil
|
||||||
|
newHeaderLines = nil
|
||||||
beof := false
|
beof := false
|
||||||
queryChanged := false
|
queryChanged := false
|
||||||
denylist := []int32{}
|
denylist := []int32{}
|
||||||
@@ -6247,6 +6340,23 @@ func (t *Terminal) Loop() error {
|
|||||||
}
|
}
|
||||||
case actPrintQuery:
|
case actPrintQuery:
|
||||||
req(reqPrintQuery)
|
req(reqPrintQuery)
|
||||||
|
case actChangeHeaderLines, actTransformHeaderLines, actBgTransformHeaderLines:
|
||||||
|
capture(true, func(expr string) {
|
||||||
|
if n, err := strconv.Atoi(expr); err == nil && n >= 0 && n != t.headerLines {
|
||||||
|
t.headerLines = n
|
||||||
|
newHeaderLines = &n
|
||||||
|
changed = true
|
||||||
|
// Deselect items that are now part of the header
|
||||||
|
for idx := range t.selected {
|
||||||
|
if idx < int32(n) {
|
||||||
|
delete(t.selected, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tell UpdateList to reposition cursor to the current item
|
||||||
|
t.targetIndex = t.currentIndex()
|
||||||
|
req(reqList, reqPrompt, reqInfo, reqHeader)
|
||||||
|
}
|
||||||
|
})
|
||||||
case actChangeMulti:
|
case actChangeMulti:
|
||||||
multi := t.multi
|
multi := t.multi
|
||||||
if a.a == "" {
|
if a.a == "" {
|
||||||
@@ -6281,6 +6391,33 @@ func (t *Terminal) Loop() error {
|
|||||||
t.forceRerenderList()
|
t.forceRerenderList()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
case actChangeWithNth, actTransformWithNth, actBgTransformWithNth:
|
||||||
|
if !t.withNthEnabled {
|
||||||
|
break Action
|
||||||
|
}
|
||||||
|
capture(true, func(expr string) {
|
||||||
|
tokens := strings.Split(expr, "|")
|
||||||
|
withNthExpr := tokens[0]
|
||||||
|
if len(tokens) > 1 {
|
||||||
|
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
|
||||||
|
}
|
||||||
|
// Empty value restores the default --with-nth
|
||||||
|
if len(withNthExpr) == 0 {
|
||||||
|
withNthExpr = t.withNthDefault
|
||||||
|
}
|
||||||
|
if withNthExpr != t.withNthExpr {
|
||||||
|
if factory, err := nthTransformer(withNthExpr); err == nil {
|
||||||
|
newWithNth = &withNthSpec{fn: factory(t.delimiter)}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.withNthExpr = withNthExpr
|
||||||
|
t.filterSelection = true
|
||||||
|
changed = true
|
||||||
|
t.clearNumLinesCache()
|
||||||
|
t.forceRerenderList()
|
||||||
|
}
|
||||||
|
})
|
||||||
case actChangeQuery:
|
case actChangeQuery:
|
||||||
t.input = []rune(a.a)
|
t.input = []rune(a.a)
|
||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
@@ -7428,7 +7565,7 @@ func (t *Terminal) Loop() error {
|
|||||||
reload := changed || newCommand != nil
|
reload := changed || newCommand != nil
|
||||||
var reloadRequest *searchRequest
|
var reloadRequest *searchRequest
|
||||||
if reload {
|
if reload {
|
||||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
|
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, withNth: newWithNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch queued background requests
|
// Dispatch queued background requests
|
||||||
|
|||||||
+18
-6
@@ -447,6 +447,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
|||||||
return dup
|
return dup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) WithNewAttr(attr Attr) ColorPair {
|
||||||
|
dup := p
|
||||||
|
dup.attr = attr
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
|
||||||
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
|
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
|
||||||
dup := p
|
dup := p
|
||||||
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
|
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
|
||||||
@@ -520,6 +526,8 @@ type ColorTheme struct {
|
|||||||
ListLabel ColorAttr
|
ListLabel ColorAttr
|
||||||
ListBorder ColorAttr
|
ListBorder ColorAttr
|
||||||
GapLine ColorAttr
|
GapLine ColorAttr
|
||||||
|
NthCurrentAttr Attr // raw current-fg attr (before fg merge) for nth overlay
|
||||||
|
NthSelectedAttr Attr // raw selected-fg attr (before ListFg inherit) for nth overlay
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -1199,13 +1207,19 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
|||||||
match.Attr = Underline
|
match.Attr = Underline
|
||||||
}
|
}
|
||||||
theme.Match = o(baseTheme.Match, match)
|
theme.Match = o(baseTheme.Match, match)
|
||||||
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
|
// These colors are not defined in the base themes.
|
||||||
|
// Resolve ListFg/ListBg early so Current and Selected can inherit from them.
|
||||||
|
theme.ListFg = o(theme.Fg, theme.ListFg)
|
||||||
|
theme.ListBg = o(theme.Bg, theme.ListBg)
|
||||||
|
// Inherit from 'list-fg', so that we don't have to write 'current-fg:dim'
|
||||||
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
||||||
current := theme.Current
|
current := theme.Current
|
||||||
if !baseTheme.Colored && current.IsUndefined() {
|
if !baseTheme.Colored && current.IsUndefined() {
|
||||||
current.Attr |= Reverse
|
current.Attr |= Reverse
|
||||||
}
|
}
|
||||||
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
|
resolvedCurrent := o(baseTheme.Current, current)
|
||||||
|
theme.NthCurrentAttr = resolvedCurrent.Attr
|
||||||
|
theme.Current = theme.ListFg.Merge(resolvedCurrent)
|
||||||
currentMatch := theme.CurrentMatch
|
currentMatch := theme.CurrentMatch
|
||||||
if !baseTheme.Colored && currentMatch.IsUndefined() {
|
if !baseTheme.Colored && currentMatch.IsUndefined() {
|
||||||
currentMatch.Attr |= Reverse | Underline
|
currentMatch.Attr |= Reverse | Underline
|
||||||
@@ -1230,10 +1244,8 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
|||||||
scrollbarDefined := theme.Scrollbar != undefined
|
scrollbarDefined := theme.Scrollbar != undefined
|
||||||
previewBorderDefined := theme.PreviewBorder != undefined
|
previewBorderDefined := theme.PreviewBorder != undefined
|
||||||
|
|
||||||
// These colors are not defined in the base themes
|
theme.NthSelectedAttr = theme.SelectedFg.Attr
|
||||||
theme.ListFg = o(theme.Fg, theme.ListFg)
|
theme.SelectedFg = theme.ListFg.Merge(theme.SelectedFg)
|
||||||
theme.ListBg = o(theme.Bg, theme.ListBg)
|
|
||||||
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
|
||||||
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
||||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||||
|
|
||||||
|
|||||||
+260
-1
@@ -1745,6 +1745,191 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth
|
||||||
|
input = [
|
||||||
|
'foo bar baz',
|
||||||
|
'aaa bbb ccc',
|
||||||
|
'xxx yyy zzz'
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
# Start with field 1 only, cycle through fields, verify $FZF_WITH_NTH via prompt
|
||||||
|
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:change-with-nth(2|3|1),result:transform-prompt:echo "[$FZF_WITH_NTH]> "' < #{tempname}), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 3, lines.item_count
|
||||||
|
assert lines.any_include?('[1]>')
|
||||||
|
assert lines.any_include?('foo')
|
||||||
|
refute lines.any_include?('bar')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('[2]>')
|
||||||
|
assert lines.any_include?('bar')
|
||||||
|
refute lines.any_include?('foo')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('[3]>')
|
||||||
|
assert lines.any_include?('baz')
|
||||||
|
refute lines.any_include?('bar')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('[1]>')
|
||||||
|
assert lines.any_include?('foo')
|
||||||
|
refute lines.any_include?('bar')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth_default
|
||||||
|
# Empty value restores the default --with-nth
|
||||||
|
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 1 --bind 'space:change-with-nth(2|)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.item_count
|
||||||
|
assert lines.any_include?('a')
|
||||||
|
refute lines.any_include?('b')
|
||||||
|
end
|
||||||
|
# Switch to field 2
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('b')
|
||||||
|
refute lines.any_include?('a')
|
||||||
|
end
|
||||||
|
# Empty restores default (field 1)
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('a')
|
||||||
|
refute lines.any_include?('b')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_transform_with_nth_search
|
||||||
|
input = [
|
||||||
|
'alpha bravo charlie',
|
||||||
|
'delta echo foxtrot',
|
||||||
|
'golf hotel india'
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:transform-with-nth(echo 2)' -q '^bravo$' < #{tempname}), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_bg_transform_with_nth_output
|
||||||
|
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 2 --bind 'space:bg-transform-with-nth(echo 3)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.item_count
|
||||||
|
assert lines.any_include?('b')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('c')
|
||||||
|
refute lines.any_include?('b')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines.any_include?('a b c') || lines.any_include?('d e f') }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth_search
|
||||||
|
input = [
|
||||||
|
'alpha bravo charlie',
|
||||||
|
'delta echo foxtrot',
|
||||||
|
'golf hotel india'
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
tmux.send_keys %(#{FZF} --with-nth 1 --bind 'space:change-with-nth(2)' -q '^bravo$' < #{tempname}), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth_output
|
||||||
|
tmux.send_keys %(echo -e 'a b c\nd e f' | #{FZF} --with-nth 2 --bind 'space:change-with-nth(3)'), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.item_count
|
||||||
|
assert lines.any_include?('b')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('c')
|
||||||
|
refute lines.any_include?('b')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines.any_include?('a b c') || lines.any_include?('d e f') }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth_selection
|
||||||
|
# Items: field1 has unique values, field2 has 'match' or 'miss'
|
||||||
|
input = [
|
||||||
|
'one match x',
|
||||||
|
'two miss y',
|
||||||
|
'three match z'
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
# Start showing field 2 (match/miss), query 'match', select all matches, then switch to field 3
|
||||||
|
tmux.send_keys %(#{FZF} --with-nth 2 --multi --bind 'ctrl-a:select-all,space:change-with-nth(3)' -q match < #{tempname}), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.match_count
|
||||||
|
end
|
||||||
|
# Select all matching items
|
||||||
|
tmux.send_keys 'C-a'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('(2)')
|
||||||
|
end
|
||||||
|
# Now change with-nth to field 3; 'x' and 'z' don't contain 'match'
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
# Selections of non-matching items should be cleared
|
||||||
|
assert lines.any_include?('(0)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_with_nth_multiline
|
||||||
|
# Each item has 3 lines: "N-a\nN-b\nN-c"
|
||||||
|
# --with-nth 1 shows 1 line per item, --with-nth 1..3 shows 3 lines per item
|
||||||
|
tmux.send_keys %(seq 20 | xargs -I{} printf '{}-a\\n{}-b\\n{}-c\\0' | #{FZF} --read0 --delimiter "\n" --with-nth 1 --bind 'space:change-with-nth(1..3|1)' --no-sort), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 20, lines.item_count
|
||||||
|
assert lines.any_include?('1-a')
|
||||||
|
refute lines.any_include?('1-b')
|
||||||
|
end
|
||||||
|
# Expand to 3 lines per item
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('1-a')
|
||||||
|
assert lines.any_include?('1-b')
|
||||||
|
assert lines.any_include?('1-c')
|
||||||
|
end
|
||||||
|
# Scroll down a few items
|
||||||
|
5.times { tmux.send_keys :Down }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('6-a')
|
||||||
|
assert lines.any_include?('6-b')
|
||||||
|
assert lines.any_include?('6-c')
|
||||||
|
end
|
||||||
|
# Collapse back to 1 line per item
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('6-a')
|
||||||
|
refute lines.any_include?('6-b')
|
||||||
|
end
|
||||||
|
# Scroll down more after collapse
|
||||||
|
5.times { tmux.send_keys :Down }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('11-a')
|
||||||
|
refute lines.any_include?('11-b')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_env_vars
|
def test_env_vars
|
||||||
def env_vars
|
def env_vars
|
||||||
return {} unless File.exist?(tempname)
|
return {} unless File.exist?(tempname)
|
||||||
@@ -1755,7 +1940,7 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
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
|
||||||
expected = {
|
expected = {
|
||||||
FZF_DIRECTION: 'down',
|
FZF_DIRECTION: 'down',
|
||||||
FZF_TOTAL_COUNT: '100',
|
FZF_TOTAL_COUNT: '100',
|
||||||
@@ -2176,6 +2361,80 @@ class TestCore < TestInteractive
|
|||||||
end
|
end
|
||||||
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
|
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.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|
|
tmux.until do |lines|
|
||||||
|
|||||||
@@ -326,4 +326,27 @@ class TestFilter < TestBase
|
|||||||
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
|
writelines(['emp001 Alice Engineering', 'emp002 Bob Marketing'])
|
||||||
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
|
assert_equal 'emp001', `#{FZF} -d' ' --with-nth 2 --accept-nth 1 -f Alice < #{tempname}`.chomp
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user