mirror of
https://github.com/junegunn/fzf.git
synced 2026-04-13 11:19:37 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce58d08ee3 | ||
|
|
997a7e5947 | ||
|
|
88e48619d6 |
@@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash,fish}]
|
||||
[*.{sh,bash}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @junegunn
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,17 +0,0 @@
|
||||
## Contribution Policy
|
||||
|
||||
We do not accept pull requests generated primarily by AI without genuine understanding or real-world usage context.
|
||||
|
||||
All contributions are expected to demonstrate:
|
||||
- A clear understanding of the codebase
|
||||
- Alignment with product direction
|
||||
- Thoughtful reasoning behind changes
|
||||
- Evidence of real-world usage or hands-on experience with the problem
|
||||
|
||||
If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
- [ ] I confirm that this PR meets the above expectations and reflects my own understanding and real-world context.
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -12,6 +12,6 @@ jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
configuration-path: .github/labeler.yml
|
||||
|
||||
9
.github/workflows/linux.yml
vendored
9
.github/workflows/linux.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
push:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master, devel ]
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.6
|
||||
|
||||
@@ -44,10 +44,5 @@ jobs:
|
||||
- name: Unit 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
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
|
||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
|
||||
4
.github/workflows/sponsors.yml
vendored
4
.github/workflows/sponsors.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||
file: 'README.md'
|
||||
|
||||
- name: Deploy to GitHub Pages 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: master
|
||||
folder: '.'
|
||||
|
||||
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4
|
||||
- uses: crate-ci/typos@v1.29.4
|
||||
|
||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: junegunn.fzf
|
||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -1,92 +1,6 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.71.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.71.0/_
|
||||
|
||||
- Added `--popup` as a new name for `--tmux` with Zellij support
|
||||
- `--popup` starts fzf in a tmux popup or a Zellij floating pane
|
||||
- `--tmux` is now an alias for `--popup`
|
||||
- Requires tmux 3.3+ or Zellij 0.44+
|
||||
- Cross-reload item identity with `--id-nth`
|
||||
- Added `--id-nth=NTH` to define item identity fields for cross-reload operations
|
||||
- When a `reload` is triggered with tracking enabled, fzf searches for the tracked item by its identity fields in the new list.
|
||||
- `--track --id-nth ..` tracks by the entire line
|
||||
- `--track --id-nth 1` tracks by the first field
|
||||
- `--track` without `--id-nth` retains the existing index-based tracking behavior
|
||||
- The UI is temporarily blocked (prompt dimmed, input disabled) until the item is found or loading completes.
|
||||
- Press `Escape` or `Ctrl-C` to cancel the blocked state without quitting
|
||||
- Info line shows `+T*` / `+t*` while searching
|
||||
- With `--multi`, selected items are preserved across `reload-sync` by matching their identity fields
|
||||
- Performance improvements
|
||||
- The search performance now scales linearly with the number of CPU cores, as we dropped static partitioning to allow better load balancing across threads.
|
||||
```
|
||||
=== query: 'linux' ===
|
||||
[all] baseline: 21.95ms current: 17.47ms (1.26x) matches: 179966 (12.79%)
|
||||
[1T] baseline: 179.63ms current: 180.53ms (1.00x) matches: 179966 (12.79%)
|
||||
[2T] baseline: 97.38ms current: 90.05ms (1.08x) matches: 179966 (12.79%)
|
||||
[4T] baseline: 53.83ms current: 44.77ms (1.20x) matches: 179966 (12.79%)
|
||||
[8T] baseline: 41.66ms current: 22.58ms (1.84x) matches: 179966 (12.79%)
|
||||
```
|
||||
- Improved the cache structure, reducing memory footprint per entry by 86x.
|
||||
- With the reduced per-entry cost, the cache now has broader coverage.
|
||||
- Shell integration improvements
|
||||
- bash: CTRL-R now supports multi-select and `shift-delete` to delete history entries (#4715)
|
||||
- fish:
|
||||
- Improved command history (CTRL-R) (#4703) (@bitraid)
|
||||
- Rewrite completion script (SHIFT-TAB) (#4731) (@bitraid)
|
||||
- Increase minimum fish version requirement to 3.4.0 (#4731) (@bitraid)
|
||||
- `GET /` HTTP endpoint now includes `positions` field in each match entry, providing the indices of matched characters for external highlighting (#4726)
|
||||
- Allow adaptive height with negative value (`--height=~-HEIGHT`) (#4682)
|
||||
- Bug fixes
|
||||
- `--walker=follow` no longer follows symlinks whose target is an ancestor of the walker root, avoiding severe resource exhaustion when a symlink points outside the tree (e.g. Wine's `z:` → `/`) (#4710)
|
||||
- Fixed AWK tokenizer not treating a new line character as whitespace
|
||||
- Fixed `--{accept,with}-nth` removing trailing whitespaces with a non-default `--delimiter`
|
||||
- Fixed OSC8 hyperlinks being mangled when the URL contains unicode characters (#4707)
|
||||
- Fixed `--with-shell` not handling quoted arguments correctly (#4709)
|
||||
- Fixed child processes not being terminated on Windows (#4723) (@pjeby)
|
||||
- Fixed preview scrollbar not rendered after `toggle-preview`
|
||||
- Fixed preview follow/scroll with long wrapped lines
|
||||
- Fixed tab width when `--frozen-left` is used
|
||||
- Fixed preview mouse events being processed when no preview window exists
|
||||
- zsh: Fixed history widget when `sh_glob` option is on (#4714) (@EvanHahn)
|
||||
|
||||
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
|
||||
------
|
||||
- Implemented word wrapping in the list section
|
||||
|
||||
@@ -112,7 +112,7 @@ the whole if we start off with `:FZF` command.
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
<
|
||||
Similarly to {ctrlp.vim}{3}, use Enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||
Similarly to {ctrlp.vim}{3}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||
selected files in the current window, in new tabs, in horizontal splits, or in
|
||||
vertical splits respectively.
|
||||
|
||||
@@ -218,6 +218,7 @@ list:
|
||||
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
|
||||
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
|
||||
`preview-fg` / `preview-bg` | Preview window text and background
|
||||
`hl` / `hl+` | Highlighted substrings (normal / current)
|
||||
`gutter` | Background of the gutter on the left
|
||||
`pointer` | Pointer to the current line ( `>` )
|
||||
`marker` | Multi-select marker ( `>` )
|
||||
@@ -228,6 +229,7 @@ list:
|
||||
`query` | Query string
|
||||
`disabled` | Query string when search is disabled
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
`pointer` | Pointer to the current line ( `>` )
|
||||
----------------------------+------------------------------------------------------
|
||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||
color when considering each of the following highlight groups
|
||||
@@ -243,7 +245,7 @@ if it exists, - otherwise use the `fg` attribute of the `Comment` highlight
|
||||
group if it exists, - otherwise fall back to the default color settings for
|
||||
the prompt.
|
||||
|
||||
You can examine the color option generated according to the setting by printing
|
||||
You can examine the color option generated according the setting by printing
|
||||
the result of `fzf#wrap()` function like so:
|
||||
>
|
||||
:echo fzf#wrap()
|
||||
|
||||
13
install
13
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.71.0
|
||||
version=0.68.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -112,15 +112,10 @@ link_fzf_in_path() {
|
||||
return 1
|
||||
}
|
||||
|
||||
tar_opts="-xzf -"
|
||||
if tar --no-same-owner -tf /dev/null 2> /dev/null; then
|
||||
tar_opts="--no-same-owner $tar_opts"
|
||||
fi
|
||||
|
||||
try_curl() {
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar $tar_opts
|
||||
curl -fL $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
@@ -130,7 +125,7 @@ try_curl() {
|
||||
try_wget() {
|
||||
command -v wget > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar $tar_opts
|
||||
wget -O - $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
@@ -444,7 +439,7 @@ if [ $update_config -eq 1 ]; then
|
||||
echo
|
||||
fi
|
||||
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fish_user_key_bindings # fish'
|
||||
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fzf_user_key_bindings # fish'
|
||||
echo
|
||||
echo 'Use uninstall script to remove fzf.'
|
||||
echo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.71.0"
|
||||
$version="0.68.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.71"
|
||||
var version = "0.68"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Apr 2026" "fzf 0.71.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Feb 2026" "fzf 0.68.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Apr 2026" "fzf 0.71.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Feb 2026" "fzf 0.68.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -134,14 +134,6 @@ e.g.
|
||||
# Use template to rearrange fields
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
||||
.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
|
||||
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||
Define which fields to print on accept. The last delimiter is stripped from the
|
||||
@@ -384,7 +376,7 @@ Use black background
|
||||
|
||||
.SS DISPLAY MODE
|
||||
.TP
|
||||
.BI "\-\-height=" "[~][\-]HEIGHT[%]"
|
||||
.BI "\-\-height=" "[~]HEIGHT[%]"
|
||||
Display fzf window below the cursor with the given height instead of using
|
||||
the full screen.
|
||||
|
||||
@@ -394,19 +386,17 @@ height minus the given value.
|
||||
fzf \-\-height=\-1
|
||||
|
||||
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
||||
range according to the input size. You can combine \fB~\fR with a negative
|
||||
value.
|
||||
range according to the input size.
|
||||
|
||||
# Will not take up 100% of the screen
|
||||
seq 5 | fzf \-\-height=~100%
|
||||
|
||||
# Adapt to input size, up to terminal height minus 1
|
||||
seq 5 | fzf \-\-height=~\-1
|
||||
|
||||
Adaptive height has the following limitations:
|
||||
.br
|
||||
* Cannot be used with top/bottom margin and padding given in percent size
|
||||
.br
|
||||
* Negative value is not allowed
|
||||
.br
|
||||
* It will not find the right size when there are multi-line items
|
||||
|
||||
.TP
|
||||
@@ -417,26 +407,25 @@ layout options so that the specified number of items are visible in the list
|
||||
section (default: \fB10+\fR).
|
||||
Ignored when \fB\-\-height\fR is not specified or set as an absolute value.
|
||||
.TP
|
||||
.BI "\-\-popup" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
|
||||
Start fzf in a tmux popup or in a Zellij floating pane (default
|
||||
\fBcenter,50%\fR). Requires tmux 3.3+ or Zellij 0.44+. This option is ignored if you
|
||||
are not running fzf inside tmux or Zellij. \fB\-\-tmux\fR is an alias for this option.
|
||||
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
|
||||
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
|
||||
later. This option is ignored if you are not running fzf inside tmux.
|
||||
|
||||
e.g.
|
||||
\fB# Popup in the center with 70% width and height
|
||||
fzf \-\-popup 70%
|
||||
fzf \-\-tmux 70%
|
||||
|
||||
# Popup on the left with 40% width and 100% height
|
||||
fzf \-\-popup right,40%
|
||||
fzf \-\-tmux right,40%
|
||||
|
||||
# Popup on the bottom with 100% width and 30% height
|
||||
fzf \-\-popup bottom,30%
|
||||
fzf \-\-tmux bottom,30%
|
||||
|
||||
# Popup on the top with 80% width and 40% height
|
||||
fzf \-\-popup top,80%,40%
|
||||
fzf \-\-tmux top,80%,40%
|
||||
|
||||
# Popup with a native tmux or Zellij border in the center with 80% width and height
|
||||
fzf \-\-popup center,80%,border\-native\fR
|
||||
# Popup with a native tmux border in the center with 80% width and height
|
||||
fzf \-\-tmux center,80%,border\-native\fR
|
||||
|
||||
.SS LAYOUT
|
||||
.TP
|
||||
@@ -620,53 +609,17 @@ Disable multi-line display of items when using \fB\-\-read0\fR
|
||||
.B "\-\-raw"
|
||||
Enable raw mode where non-matching items are also displayed in a dimmed color.
|
||||
.TP
|
||||
.BI "\-\-track"
|
||||
.B "\-\-track"
|
||||
Make fzf track the current selection when the result list is updated.
|
||||
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||
not recommended to use this option with \fB\-\-tac\fR as the resulting behavior
|
||||
can be confusing.
|
||||
|
||||
When \fB\-\-id\-nth\fR is also set, fzf enables field\-based tracking across
|
||||
\fBreload\fRs. See \fB\-\-id\-nth\fR for details.
|
||||
|
||||
Without \fB\-\-id\-nth\fR, \fB\-\-track\fR uses index\-based tracking that
|
||||
does not persist across reloads.
|
||||
can be confusing. Also, consider using \fBtrack\fR action instead of this
|
||||
option.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fB# Index\-based tracking (does not persist across reloads)
|
||||
git log \-\-oneline \-\-graph \-\-color=always | nl |
|
||||
\fBgit log \-\-oneline \-\-graph \-\-color=always | nl |
|
||||
fzf \-\-ansi \-\-track \-\-no\-sort \-\-layout=reverse\-list\fR
|
||||
|
||||
\fB# Track by first field (e.g. pod name) across reloads
|
||||
kubectl get pods | fzf \-\-track \-\-id\-nth 1 \-\-header\-lines=1 \\
|
||||
\-\-bind 'ctrl\-r:reload:kubectl get pods'\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-id\-nth=" "N[,..]"
|
||||
Define item identity fields for cross\-reload operations. When set, fzf
|
||||
uses the specified fields to identify items across \fBreload\fR and
|
||||
\fBreload\-sync\fR.
|
||||
|
||||
With \fB\-\-track\fR, fzf extracts the tracking key from the current item
|
||||
using the nth expression and searches for a matching item in the reloaded list.
|
||||
While searching, the UI is blocked (query input and cursor movement are
|
||||
disabled, and the prompt is dimmed). With \fBreload\fR, the blocked state
|
||||
clears as soon as the match is found in the stream. With \fBreload\-sync\fR,
|
||||
the blocked state persists until the entire stream is complete. Press
|
||||
\fBEscape\fR or \fBCtrl\-C\fR to cancel the blocked state without quitting fzf.
|
||||
|
||||
The info line shows \fB+T*\fR (or \fB+t*\fR for one\-off tracking) while
|
||||
the search is in progress.
|
||||
|
||||
With \fB\-\-multi\fR, selected items are preserved across \fBreload\-sync\fR
|
||||
by matching their identity fields in the reloaded list.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fB# Track and preserve selections by pod name across reloads
|
||||
kubectl get pods | fzf \-\-multi \-\-track \-\-id\-nth 1 \-\-header\-lines=1 \\
|
||||
\-\-bind 'ctrl\-r:reload\-sync:kubectl get pods'\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-tac"
|
||||
@@ -1276,18 +1229,6 @@ Here is an example script that uses a Unix socket instead of a TCP port.
|
||||
curl --unix-socket /tmp/fzf.sock http -d up
|
||||
\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
|
||||
.TP
|
||||
.B "\-\-walker=[file][,dir][,follow][,hidden]"
|
||||
@@ -1457,8 +1398,6 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_NTH " Current \-\-nth option"
|
||||
.br
|
||||
.BR FZF_WITH_NTH " Current \-\-with\-nth option"
|
||||
.br
|
||||
.BR FZF_PROMPT " Prompt string"
|
||||
.br
|
||||
.BR FZF_GHOST " Ghost string"
|
||||
@@ -1949,7 +1888,6 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\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\-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\-preview(...)\fR (change \fB\-\-preview\fR option)
|
||||
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
|
||||
@@ -2055,7 +1993,6 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtransform\-input\-label(...)\fR (transform input 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\-with\-nth(...)\fR (transform with-nth 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\-prompt(...)\fR (transform prompt string using an external command)
|
||||
|
||||
141
shell/common.fish
Normal file
141
shell/common.fish
Normal file
@@ -0,0 +1,141 @@
|
||||
function __fzf_defaults
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
# Get tokens - use version-appropriate flags
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
# Filter out leading environment variable assignments
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
# Skip command prefixes so callers see the actual command name,
|
||||
# e.g. "builtin cd" → "cd", "env VAR=1 command cd" → "cd"
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
# Set variables containing the major and minor fish version numbers, using
|
||||
# a method compatible with all supported fish versions.
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
# fish v3.5.0 and newer
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.4.1
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish v3.1b1 - v3.1.2
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
@@ -1,9 +1,9 @@
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
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
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.bash
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
@@ -36,9 +38,9 @@ if [[ $- =~ i ]]; then
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
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
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
@@ -79,7 +81,7 @@ __fzf_orig_completion() {
|
||||
f="${BASH_REMATCH[2]}"
|
||||
cmd="${BASH_REMATCH[3]}"
|
||||
[[ $f == _fzf_* ]] && continue
|
||||
builtin printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||
fi
|
||||
@@ -109,7 +111,7 @@ __fzf_orig_completion_instantiate() {
|
||||
orig="${!orig_var-}"
|
||||
orig="${orig%#*}"
|
||||
[[ $orig == *' %s '* ]] || return 1
|
||||
builtin printf -v REPLY "$orig" "$func"
|
||||
printf -v REPLY "$orig" "$func"
|
||||
}
|
||||
|
||||
_fzf_opts_completion() {
|
||||
@@ -159,7 +161,6 @@ _fzf_opts_completion() {
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--id-nth
|
||||
--info
|
||||
--info-command
|
||||
--input-border
|
||||
@@ -375,7 +376,7 @@ __fzf_generic_path_completion() {
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
if declare -F "$1" > /dev/null; then
|
||||
eval "$1 $(builtin printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
@@ -384,7 +385,7 @@ __fzf_generic_path_completion() {
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
builtin printf "%q " "${item%$3}$3"
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
@@ -394,9 +395,9 @@ __fzf_generic_path_completion() {
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
# To redraw line after fzf closes (builtin printf '\e[5n')
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
builtin printf '\e[5n'
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(command dirname "$dir")
|
||||
@@ -454,7 +455,7 @@ _fzf_complete() {
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
builtin printf '\e[5n'
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||
@@ -526,7 +527,7 @@ _fzf_proc_completion_post() {
|
||||
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
|
||||
# local COMPREPLY=()
|
||||
# _known_hosts_real ''
|
||||
# builtin printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
||||
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
||||
# }
|
||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
|
||||
@@ -4,166 +4,238 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.fish
|
||||
#
|
||||
# - $FZF_COMPLETION_OPTS
|
||||
# - $FZF_EXPANSION_OPTS
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
||||
# displayed on older versions, the command substitution syntax $() should not
|
||||
# be used anywhere in the script, otherwise the source command will fail.
|
||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
||||
echo "fzf completion script requires fish version 3.4.0 or newer." >&2
|
||||
return 1
|
||||
else if not command -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
function fzf_completion_setup
|
||||
|
||||
function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search'
|
||||
# Restore the default shift-tab behavior on tab completions
|
||||
if commandline --paging-mode
|
||||
commandline -f complete-and-search
|
||||
return
|
||||
#----BEGIN INCLUDE common.fish
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.fish" for the implementation details.
|
||||
|
||||
function __fzf_defaults
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
# Remove any trailing unescaped backslash from token and update command line
|
||||
set -l -- token (string replace -r -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\$' '' (commandline -t | string collect) | string collect)
|
||||
commandline -rt -- $token
|
||||
|
||||
# Remove any line breaks from token
|
||||
set -- token (string replace -ra -- '\\\\\\n' '' $token | string collect)
|
||||
|
||||
# regex: Match token with unescaped/unquoted glob character
|
||||
set -l -- r_glob '^(?:[^\'"\\\\*]|\\\\[\\S\\s]|\'(?:\\\\[\\S\\s]|[^\'\\\\])*\'|"(?:\\\\[\\S\\s]|[^"\\\\])*")*\\*[\\S\\s]*$'
|
||||
|
||||
# regex: Match any unbalanced quote character
|
||||
set -l -- r_quote '^(?>(?:\\\\[\\s\\S]|"(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\'|[^\'"\\\\]+)*)\\K[\'"]'
|
||||
|
||||
# The expansion pattern is the token with any open quote closed, or is empty.
|
||||
set -l -- glob_pattern (string match -r -- $r_glob $token | string collect)(string match -r -- $r_quote $token | string collect -a)
|
||||
|
||||
set -l -- cl_tokenize_opt '--tokens-expanded'
|
||||
string match -q -- '3.*' $version
|
||||
and set -- cl_tokenize_opt '--tokenize'
|
||||
|
||||
# Set command line tokens without any leading variable definitions or launcher
|
||||
# commands (including their options, but not any option arguments).
|
||||
set -l -- r_cmd '^(?:(?:builtin|command|doas|env|sudo|\\w+=\\S*|-\\S+)\\s+)*\\K[\\s\\S]+'
|
||||
set -l -- cmd (commandline $cl_tokenize_opt --input=(commandline -pc | string match -r $r_cmd))
|
||||
test -z "$token"
|
||||
and set -a -- cmd ''
|
||||
|
||||
# Set fzf options
|
||||
test -z "$FZF_TMUX_HEIGHT"
|
||||
and set -l -- FZF_TMUX_HEIGHT 40%
|
||||
|
||||
set -lax -- FZF_DEFAULT_OPTS \
|
||||
"--height=$FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS '--bind=alt-r:toggle-raw --multi --wrap=word --reverse' \
|
||||
(if test -n "$glob_pattern"; string collect -- $FZF_EXPANSION_OPTS; else;
|
||||
string collect -- $FZF_COMPLETION_OPTS; end; string escape -n -- $argv) \
|
||||
--with-shell=(status fish-path)\\ -c
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l -- fzf_cmd fzf
|
||||
test "$FZF_TMUX" = 1
|
||||
and set -- fzf_cmd fzf-tmux $FZF_TMUX_OPTS -d$FZF_TMUX_HEIGHT --
|
||||
|
||||
set -l result
|
||||
|
||||
# Get the completion list from stdin when it's not a tty
|
||||
if not isatty stdin
|
||||
set -l -- custom_post_func _fzf_post_complete_$cmd[1]
|
||||
functions -q $custom_post_func
|
||||
or set -- custom_post_func _fzf_complete_$cmd[1]_post
|
||||
|
||||
if functions -q $custom_post_func
|
||||
$fzf_cmd | $custom_post_func $cmd | while read -l r; set -a -- result $r; end
|
||||
else if string match -q -- '*--print0*' "$FZF_DEFAULT_OPTS"
|
||||
$fzf_cmd | while read -lz r; set -a -- result $r; end
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
$fzf_cmd | while read -l r; set -a -- result $r; end
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
# Wildcard expansion
|
||||
else if test -n "$glob_pattern"
|
||||
# Set the command to be run by fzf, so there is a visual indicator and an
|
||||
# easy way to abort on long recursive searches.
|
||||
set -lx -- FZF_DEFAULT_COMMAND "for i in $glob_pattern;" \
|
||||
'test -d "$i"; and string match -qv -- "*/" $i; and set -- i $i/;' \
|
||||
'string join0 -- $i; end'
|
||||
|
||||
set -- result (string escape -n -- ($fzf_cmd --read0 --print0 --scheme=path --no-multi-line | string split0))
|
||||
|
||||
# Command completion
|
||||
else
|
||||
# Call custom function if defined
|
||||
set -l -- custom_func _fzf_complete_$cmd[1]
|
||||
if functions -q $custom_func; and not set -q __fzf_no_custom_complete
|
||||
set -lx __fzf_no_custom_complete
|
||||
$custom_func $cmd
|
||||
return
|
||||
end
|
||||
|
||||
# Workaround for complete not having newlines in results
|
||||
if string match -qr -- '\\n' $token
|
||||
set -- token (string replace -ra -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\\$' '\\\\\\\\\$' $token | string collect)
|
||||
set -- token (string unescape -- $token | string collect)
|
||||
set -- token (string replace -ra -- '\\n' '\\\\n' $token | string collect)
|
||||
end
|
||||
|
||||
set -- list (complete -C --escape -- (string join -- ' ' (commandline -pc $cl_tokenize_opt) $token | string collect))
|
||||
if test -n "$list"
|
||||
# Get the initial tabstop value
|
||||
if set -l -- tabstop (string match -rga -- '--tabstop[= ](?:0*)([1-9]\\d+|[4-9])' "$FZF_DEFAULT_OPTS")[-1]
|
||||
set -- tabstop (math $tabstop - 4)
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
set -- tabstop 4
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
# Determine the tabstop length for description alignment
|
||||
set -l -- max_columns (math $COLUMNS - 40)
|
||||
for i in $list[1..500]
|
||||
set -l -- item (string split -f 1 -- \t $i)
|
||||
and set -l -- len (string length -V -- $item)
|
||||
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
|
||||
and set -- tabstop $len
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
set -- tabstop (math $tabstop + 4)
|
||||
end
|
||||
|
||||
set -- result (string collect -- $list | $fzf_cmd --delimiter="\t" --tabstop=$tabstop --wrap-sign=\t"↳ " --accept-nth=1)
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
#----END INCLUDE
|
||||
|
||||
# Use complete builtin for specific commands
|
||||
function __fzf_complete_native
|
||||
set -l -- token (commandline -t)
|
||||
set -l -- completions (eval complete -C \"$argv[1]\")
|
||||
test -n "$completions"; or begin commandline -f repaint; return; end
|
||||
|
||||
# Calculate tabstop based on longest completion item (sample first 500 for performance)
|
||||
set -l -- tabstop 20
|
||||
set -l -- sample_size (math "min(500, "(count $completions)")")
|
||||
for c in $completions[1..$sample_size]
|
||||
set -l -- len (string length -V -- (string split -- \t $c))
|
||||
test -n "$len[2]" -a "$len[1]" -gt "$tabstop"
|
||||
and set -- tabstop $len[1]
|
||||
end
|
||||
# limit to 120 to prevent long lines
|
||||
set -- tabstop (math "min($tabstop + 4, 120)")
|
||||
|
||||
set -l result
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --delimiter=\\t --nth=1 --tabstop=$tabstop --color=fg:dim,nth:regular" \
|
||||
$FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1 --read0 --print0)
|
||||
set -- result (string join0 -- $completions | eval (__fzfcmd) | string split0)
|
||||
and begin
|
||||
set -l -- tail ' '
|
||||
# Append / to bare ~username results (fish omits it unlike other shells)
|
||||
set -- result (string replace -r -- '^(~\w+)\s?$' '$1/' $result)
|
||||
# Don't add trailing space if single result is a directory
|
||||
test (count $result) -eq 1
|
||||
and string match -q -- '*/' "$result"; and set -- tail ''
|
||||
|
||||
set -l -- result (string escape -n -- $result)
|
||||
|
||||
string match -q -- '~*' "$token"
|
||||
and set result (string replace -r -- '^\\\\~' '~' $result)
|
||||
|
||||
string match -q -- '$*' "$token"
|
||||
and set result (string replace -r -- '^\\\\\$' '\$' $result)
|
||||
|
||||
commandline -rt -- (string join ' ' -- $result)$tail
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function _fzf_complete
|
||||
set -l -- args (string escape -- $argv | string join ' ' | string split -- ' -- ')
|
||||
set -l -- post_func (status function)_(string split -- ' ' $args[2])[1]_post
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS $args[1])
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
set -l -- fzf_query (commandline -t | string escape)
|
||||
set -l result
|
||||
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
|
||||
and if functions -q $post_func
|
||||
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
|
||||
else
|
||||
commandline -rt -- (string join -- ' ' (string escape -- $result))' '
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
# Kill completion (process selection)
|
||||
function _fzf_complete_kill
|
||||
set -l -- fzf_query (commandline -t | string escape)
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
|
||||
--accept-nth=2 -m --header-lines=1 --no-preview --wrap)
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
if type -q ps
|
||||
set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
|
||||
'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
|
||||
'or command ps --everyone --full --windows 2>/dev/null; end'
|
||||
set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$fzf_query)
|
||||
and commandline -rt -- (string join ' ' -- $result)" "
|
||||
else
|
||||
__fzf_complete_native "kill " --multi --query=$fzf_query
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
# Main completion function
|
||||
function fzf-completion
|
||||
set -l -- tokens (__fzf_cmd_tokens)
|
||||
set -l -- current_token (commandline -t)
|
||||
set -l -- cmd_name $tokens[1]
|
||||
|
||||
# Route to appropriate completion function
|
||||
if test -n "$tokens"; and functions -q _fzf_complete_$cmd_name
|
||||
_fzf_complete_$cmd_name $tokens
|
||||
else
|
||||
set -l -- fzf_opt --query=$current_token --multi
|
||||
__fzf_complete_native "$tokens $current_token" $fzf_opt
|
||||
end
|
||||
end
|
||||
|
||||
# Update command line
|
||||
if test -n "$result"
|
||||
# No extra space after single selection that ends with path separator
|
||||
set -l -- tail ' '
|
||||
test (count $result) -eq 1
|
||||
and string match -q -- '*/' "$result"
|
||||
and set -- tail ''
|
||||
|
||||
commandline -rt -- (string join -- ' ' $result)$tail
|
||||
# Bind Shift-Tab to fzf-completion (Tab retains native Fish behavior)
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
bind shift-tab fzf-completion
|
||||
bind -M insert shift-tab fzf-completion
|
||||
else
|
||||
bind -k btab fzf-completion
|
||||
bind -M insert -k btab fzf-completion
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function _fzf_complete
|
||||
set -l fzf_args
|
||||
for i in $argv
|
||||
string match -q -- '--' $i; and break
|
||||
set -a -- fzf_args $i
|
||||
end
|
||||
fzf_complete $fzf_args
|
||||
end
|
||||
|
||||
# Bind to shift-tab
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
bind shift-tab fzf_complete
|
||||
bind -M insert shift-tab fzf_complete
|
||||
else
|
||||
bind -k btab fzf_complete
|
||||
bind -M insert -k btab fzf_complete
|
||||
end
|
||||
# Run setup
|
||||
fzf_completion_setup
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.zsh
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
@@ -100,9 +102,9 @@ if [[ -o interactive ]]; then
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
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
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.bash
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
@@ -24,9 +25,9 @@ if [[ $- =~ i ]]; then
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
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
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
@@ -76,31 +77,17 @@ __fzf_cd__() {
|
||||
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
||||
}
|
||||
|
||||
__fzf_history_delete() {
|
||||
[[ -s $1 ]] || return
|
||||
|
||||
local offsets
|
||||
offsets=($(sort -rnu "$1"))
|
||||
for offset in "${offsets[@]}"; do
|
||||
builtin history -d "$offset"
|
||||
done
|
||||
}
|
||||
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output script deletefile
|
||||
deletefile=$(mktemp)
|
||||
local output script
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line --bind 'shift-delete:execute-silent(cat {+f1} >> \"$deletefile\")+exclude-multi' --multi ${FZF_CTRL_R_OPTS-} --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
)
|
||||
__fzf_history_delete "$deletefile"
|
||||
command rm -f "$deletefile"
|
||||
[[ -n $output ]] || return
|
||||
) || return
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
@@ -110,8 +97,7 @@ if command -v perl > /dev/null; then
|
||||
}
|
||||
else # awk - fallback for POSIX systems
|
||||
__fzf_history__() {
|
||||
local output script deletefile
|
||||
deletefile=$(mktemp)
|
||||
local output script
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||
NR==1 { b = substr($0, 2); next }
|
||||
@@ -122,12 +108,9 @@ else # awk - fallback for POSIX systems
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line --bind 'shift-delete:execute-silent(cat {+f1} >> \"$deletefile\")+exclude-multi' --multi ${FZF_CTRL_R_OPTS-} --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
)
|
||||
__fzf_history_delete "$deletefile"
|
||||
command rm -f "$deletefile"
|
||||
[[ -n $output ]] || return
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.fish
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
@@ -11,29 +12,35 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
|
||||
# command substitution syntax $(cmd) should never be used, even behind a version
|
||||
# check, otherwise the source command will fail on fish versions older than 3.4.0.
|
||||
function fzf_key_bindings
|
||||
|
||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
||||
# displayed on older versions, the command substitution syntax $() should not
|
||||
# be used anywhere in the script, otherwise the source command will fail.
|
||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
||||
echo "fzf key bindings script requires fish version 3.4.0 or newer." >&2
|
||||
# Check fish version
|
||||
if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
|
||||
and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
|
||||
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||
return 1
|
||||
else if not command -q fzf
|
||||
else if not type -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
#----BEGIN INCLUDE common.fish
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.fish".
|
||||
# To modify it, one can edit "common.fish" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.fish" for the implementation details.
|
||||
|
||||
function __fzf_defaults
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..]
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
@@ -47,59 +54,107 @@ function fzf_key_bindings
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_cmd_tokens -d 'Return command line tokens, skipping leading env assignments and command prefixes'
|
||||
set -l tokens
|
||||
if test (string match -r -- '^\d+' $version) -ge 4
|
||||
set -- tokens (commandline -xpc)
|
||||
else
|
||||
set -- tokens (commandline -opc)
|
||||
end
|
||||
|
||||
set -l -- var_count 0
|
||||
for i in $tokens
|
||||
if string match -qr -- '^[\w]+=' $i
|
||||
set var_count (math $var_count + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
set -e -- tokens[0..$var_count]
|
||||
|
||||
while true
|
||||
switch "$tokens[1]"
|
||||
case builtin command
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
case env
|
||||
set -e -- tokens[1]
|
||||
test "$tokens[1]" = "--"; and set -e -- tokens[1]
|
||||
while string match -qr -- '^[\w]+=' "$tokens[1]"
|
||||
set -e -- tokens[1]
|
||||
end
|
||||
case '*'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- $tokens
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
set -l -- match_regex '(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\\s=]+=|^-(?!-)\\S'
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
# Don't use option prefix if " -- " is preceded.
|
||||
string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
# fish v4.0.0 and newer
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\\(?=~)|\\\\(?=\\$\\w)' '')
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if string match -qr -- '^\\d\\d+|^4|^3\\.[5-9]' $version
|
||||
# fish v3.5.0 and newer
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -N)
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query
|
||||
else
|
||||
string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
if test "$fish_major" -ge 4
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
#----END INCLUDE
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
@@ -116,7 +171,7 @@ function fzf_key_bindings
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape --no-quoted -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
@@ -128,33 +183,45 @@ function fzf_key_bindings
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \
|
||||
'--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \
|
||||
'--bind="alt-enter:become(set -g fzf_temp {+sf3..}; string join0 -- (string split0 -- <$fzf_temp | fish_indent -i); unlink $fzf_temp &>/dev/null)"' \
|
||||
'--bind="alt-t:change-with-nth(1,3..|3..|2..)"' \
|
||||
'--bind="shift-delete:execute-silent(eval builtin history delete -Ce -- (string escape -n -- (string split0 -- <{+sf3..})))+reload(eval $FZF_DEFAULT_COMMAND)"' \
|
||||
'--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
|
||||
'--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
||||
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
|
||||
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||
'--accept-nth=3.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
'--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
|
||||
# Add dynamic preview options if preview command isn't already set by user
|
||||
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
||||
# Prepend the options to allow user overrides
|
||||
# Convert the highlighted timestamp using the date command if available
|
||||
set -l -- date_cmd '{1}'
|
||||
if type -q date
|
||||
if date -d @0 '+%s' 2>/dev/null | string match -q 0
|
||||
# GNU date
|
||||
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
|
||||
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
|
||||
# BSD date
|
||||
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
|
||||
end
|
||||
end
|
||||
|
||||
# Prepend the options to allow user customizations
|
||||
set -p -- FZF_DEFAULT_OPTS \
|
||||
'--bind="focus,multi,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string join0 -- <{f3..} | string length) -gt (math $FZF_COLUMNS - (switch $FZF_WITH_NTH; case 2..; echo 13; case 1,3..; echo 25; case 3..; echo 1; end)) \\\\) -o (string split0 -- <{sf3..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else; echo hide-preview; end"' \
|
||||
'--preview="test \\"$FZF_SELECT_COUNT\\" -gt 0; and string split0 -- <{+sf3..} | fish_indent (string match -q -- 3.\\\\* $version; or echo -- --only-indent) --ansi; and echo -n \\\\n; string collect -- \\\\#\\\\ {1} (string split0 -- <{sf3..}) | fish_indent --ansi"' \
|
||||
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
|
||||
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
|
||||
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
||||
end
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z'
|
||||
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
|
||||
|
||||
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
||||
if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $version
|
||||
if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
|
||||
and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
|
||||
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
|
||||
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
|
||||
|
||||
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
||||
set -a -- FZF_DEFAULT_COMMAND '--color=always --show-time=(set_color $fish_color_comment)"%F %a %T%t%s%t"(set_color $fish_color_normal)'
|
||||
else
|
||||
set -a -- FZF_DEFAULT_COMMAND '--show-time="%F %a %T%t%s%t"'
|
||||
set -a -- FZF_DEFAULT_COMMAND '--color=always'
|
||||
end
|
||||
|
||||
# Merge history from other sessions before searching
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.zsh
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
@@ -44,9 +45,9 @@ if [[ -o interactive ]]; then
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
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
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
@@ -128,7 +129,7 @@ fi
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected extracted_with_perl=0
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_sh_glob no_ksharrays extendedglob 2> /dev/null
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
|
||||
@@ -8,24 +8,26 @@ dir=${0%"${0##*/}"}
|
||||
|
||||
update() {
|
||||
{
|
||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
||||
sed -n "1,/^#----BEGIN INCLUDE $1/p" "$2"
|
||||
cat << EOF
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
# NOTE: Do not directly edit this section, which is copied from "$1".
|
||||
# To modify it, one can edit "$1" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "$1" for the implementation details.
|
||||
EOF
|
||||
echo
|
||||
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
|
||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
||||
} > "$1.part"
|
||||
grep -v '^[[:blank:]]*#' "$dir/$1" # remove code comments from the common file
|
||||
sed -n '/^#----END INCLUDE/,$p' "$2"
|
||||
} > "$2.part"
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
mv -f "$2.part" "$2"
|
||||
}
|
||||
|
||||
update "$dir/completion.bash"
|
||||
update "$dir/completion.zsh"
|
||||
update "$dir/key-bindings.bash"
|
||||
update "$dir/key-bindings.zsh"
|
||||
update "common.sh" "$dir/completion.bash"
|
||||
update "common.sh" "$dir/completion.zsh"
|
||||
update "common.sh" "$dir/key-bindings.bash"
|
||||
update "common.sh" "$dir/key-bindings.zsh"
|
||||
update "common.fish" "$dir/completion.fish"
|
||||
update "common.fish" "$dir/key-bindings.fish"
|
||||
|
||||
# Check if --check is in ARGV
|
||||
check=0
|
||||
|
||||
@@ -38,159 +38,156 @@ func _() {
|
||||
_ = x[actChangeListLabel-27]
|
||||
_ = x[actChangeMulti-28]
|
||||
_ = x[actChangeNth-29]
|
||||
_ = x[actChangeWithNth-30]
|
||||
_ = x[actChangePointer-31]
|
||||
_ = x[actChangePreview-32]
|
||||
_ = x[actChangePreviewLabel-33]
|
||||
_ = x[actChangePreviewWindow-34]
|
||||
_ = x[actChangePrompt-35]
|
||||
_ = x[actChangeQuery-36]
|
||||
_ = x[actClearScreen-37]
|
||||
_ = x[actClearQuery-38]
|
||||
_ = x[actClearSelection-39]
|
||||
_ = x[actClose-40]
|
||||
_ = x[actDeleteChar-41]
|
||||
_ = x[actDeleteCharEof-42]
|
||||
_ = x[actEndOfLine-43]
|
||||
_ = x[actFatal-44]
|
||||
_ = x[actForwardChar-45]
|
||||
_ = x[actForwardWord-46]
|
||||
_ = x[actForwardSubWord-47]
|
||||
_ = x[actKillLine-48]
|
||||
_ = x[actKillWord-49]
|
||||
_ = x[actKillSubWord-50]
|
||||
_ = x[actUnixLineDiscard-51]
|
||||
_ = x[actUnixWordRubout-52]
|
||||
_ = x[actYank-53]
|
||||
_ = x[actBackwardKillWord-54]
|
||||
_ = x[actBackwardKillSubWord-55]
|
||||
_ = x[actSelectAll-56]
|
||||
_ = x[actDeselectAll-57]
|
||||
_ = x[actToggle-58]
|
||||
_ = x[actToggleSearch-59]
|
||||
_ = x[actToggleAll-60]
|
||||
_ = x[actToggleDown-61]
|
||||
_ = x[actToggleUp-62]
|
||||
_ = x[actToggleIn-63]
|
||||
_ = x[actToggleOut-64]
|
||||
_ = x[actToggleTrack-65]
|
||||
_ = x[actToggleTrackCurrent-66]
|
||||
_ = x[actToggleHeader-67]
|
||||
_ = x[actToggleWrap-68]
|
||||
_ = x[actToggleWrapWord-69]
|
||||
_ = x[actToggleMultiLine-70]
|
||||
_ = x[actToggleHscroll-71]
|
||||
_ = x[actToggleRaw-72]
|
||||
_ = x[actEnableRaw-73]
|
||||
_ = x[actDisableRaw-74]
|
||||
_ = x[actTrackCurrent-75]
|
||||
_ = x[actToggleInput-76]
|
||||
_ = x[actHideInput-77]
|
||||
_ = x[actShowInput-78]
|
||||
_ = x[actUntrackCurrent-79]
|
||||
_ = x[actDown-80]
|
||||
_ = x[actDownMatch-81]
|
||||
_ = x[actUp-82]
|
||||
_ = x[actUpMatch-83]
|
||||
_ = x[actPageUp-84]
|
||||
_ = x[actPageDown-85]
|
||||
_ = x[actPosition-86]
|
||||
_ = x[actHalfPageUp-87]
|
||||
_ = x[actHalfPageDown-88]
|
||||
_ = x[actOffsetUp-89]
|
||||
_ = x[actOffsetDown-90]
|
||||
_ = x[actOffsetMiddle-91]
|
||||
_ = x[actJump-92]
|
||||
_ = x[actJumpAccept-93]
|
||||
_ = x[actPrintQuery-94]
|
||||
_ = x[actRefreshPreview-95]
|
||||
_ = x[actReplaceQuery-96]
|
||||
_ = x[actToggleSort-97]
|
||||
_ = x[actShowPreview-98]
|
||||
_ = x[actHidePreview-99]
|
||||
_ = x[actTogglePreview-100]
|
||||
_ = x[actTogglePreviewWrap-101]
|
||||
_ = x[actTogglePreviewWrapWord-102]
|
||||
_ = x[actTransform-103]
|
||||
_ = x[actTransformBorderLabel-104]
|
||||
_ = x[actTransformGhost-105]
|
||||
_ = x[actTransformHeader-106]
|
||||
_ = x[actTransformHeaderLines-107]
|
||||
_ = x[actTransformFooter-108]
|
||||
_ = x[actTransformHeaderLabel-109]
|
||||
_ = x[actTransformFooterLabel-110]
|
||||
_ = x[actTransformInputLabel-111]
|
||||
_ = x[actTransformListLabel-112]
|
||||
_ = x[actTransformNth-113]
|
||||
_ = x[actTransformWithNth-114]
|
||||
_ = x[actTransformPointer-115]
|
||||
_ = x[actTransformPreviewLabel-116]
|
||||
_ = x[actTransformPrompt-117]
|
||||
_ = x[actTransformQuery-118]
|
||||
_ = x[actTransformSearch-119]
|
||||
_ = x[actTrigger-120]
|
||||
_ = x[actBgTransform-121]
|
||||
_ = x[actBgTransformBorderLabel-122]
|
||||
_ = x[actBgTransformGhost-123]
|
||||
_ = x[actBgTransformHeader-124]
|
||||
_ = x[actBgTransformHeaderLines-125]
|
||||
_ = x[actBgTransformFooter-126]
|
||||
_ = x[actBgTransformHeaderLabel-127]
|
||||
_ = x[actBgTransformFooterLabel-128]
|
||||
_ = x[actBgTransformInputLabel-129]
|
||||
_ = x[actBgTransformListLabel-130]
|
||||
_ = x[actBgTransformNth-131]
|
||||
_ = x[actBgTransformWithNth-132]
|
||||
_ = x[actBgTransformPointer-133]
|
||||
_ = x[actBgTransformPreviewLabel-134]
|
||||
_ = x[actBgTransformPrompt-135]
|
||||
_ = x[actBgTransformQuery-136]
|
||||
_ = x[actBgTransformSearch-137]
|
||||
_ = x[actBgCancel-138]
|
||||
_ = x[actSearch-139]
|
||||
_ = x[actPreview-140]
|
||||
_ = x[actPreviewTop-141]
|
||||
_ = x[actPreviewBottom-142]
|
||||
_ = x[actPreviewUp-143]
|
||||
_ = x[actPreviewDown-144]
|
||||
_ = x[actPreviewPageUp-145]
|
||||
_ = x[actPreviewPageDown-146]
|
||||
_ = x[actPreviewHalfPageUp-147]
|
||||
_ = x[actPreviewHalfPageDown-148]
|
||||
_ = x[actPrevHistory-149]
|
||||
_ = x[actPrevSelected-150]
|
||||
_ = x[actPrint-151]
|
||||
_ = x[actPut-152]
|
||||
_ = x[actNextHistory-153]
|
||||
_ = x[actNextSelected-154]
|
||||
_ = x[actExecute-155]
|
||||
_ = x[actExecuteSilent-156]
|
||||
_ = x[actExecuteMulti-157]
|
||||
_ = x[actSigStop-158]
|
||||
_ = x[actBest-159]
|
||||
_ = x[actFirst-160]
|
||||
_ = x[actLast-161]
|
||||
_ = x[actReload-162]
|
||||
_ = x[actReloadSync-163]
|
||||
_ = x[actDisableSearch-164]
|
||||
_ = x[actEnableSearch-165]
|
||||
_ = x[actSelect-166]
|
||||
_ = x[actDeselect-167]
|
||||
_ = x[actUnbind-168]
|
||||
_ = x[actRebind-169]
|
||||
_ = x[actToggleBind-170]
|
||||
_ = x[actBecome-171]
|
||||
_ = x[actShowHeader-172]
|
||||
_ = x[actHideHeader-173]
|
||||
_ = x[actBell-174]
|
||||
_ = x[actExclude-175]
|
||||
_ = x[actExcludeMulti-176]
|
||||
_ = x[actAsync-177]
|
||||
_ = x[actChangePointer-30]
|
||||
_ = x[actChangePreview-31]
|
||||
_ = x[actChangePreviewLabel-32]
|
||||
_ = x[actChangePreviewWindow-33]
|
||||
_ = x[actChangePrompt-34]
|
||||
_ = x[actChangeQuery-35]
|
||||
_ = x[actClearScreen-36]
|
||||
_ = x[actClearQuery-37]
|
||||
_ = x[actClearSelection-38]
|
||||
_ = x[actClose-39]
|
||||
_ = x[actDeleteChar-40]
|
||||
_ = x[actDeleteCharEof-41]
|
||||
_ = x[actEndOfLine-42]
|
||||
_ = x[actFatal-43]
|
||||
_ = x[actForwardChar-44]
|
||||
_ = x[actForwardWord-45]
|
||||
_ = x[actForwardSubWord-46]
|
||||
_ = x[actKillLine-47]
|
||||
_ = x[actKillWord-48]
|
||||
_ = x[actKillSubWord-49]
|
||||
_ = x[actUnixLineDiscard-50]
|
||||
_ = x[actUnixWordRubout-51]
|
||||
_ = x[actYank-52]
|
||||
_ = x[actBackwardKillWord-53]
|
||||
_ = x[actBackwardKillSubWord-54]
|
||||
_ = x[actSelectAll-55]
|
||||
_ = x[actDeselectAll-56]
|
||||
_ = x[actToggle-57]
|
||||
_ = x[actToggleSearch-58]
|
||||
_ = x[actToggleAll-59]
|
||||
_ = x[actToggleDown-60]
|
||||
_ = x[actToggleUp-61]
|
||||
_ = x[actToggleIn-62]
|
||||
_ = x[actToggleOut-63]
|
||||
_ = x[actToggleTrack-64]
|
||||
_ = x[actToggleTrackCurrent-65]
|
||||
_ = x[actToggleHeader-66]
|
||||
_ = x[actToggleWrap-67]
|
||||
_ = x[actToggleWrapWord-68]
|
||||
_ = x[actToggleMultiLine-69]
|
||||
_ = x[actToggleHscroll-70]
|
||||
_ = x[actToggleRaw-71]
|
||||
_ = x[actEnableRaw-72]
|
||||
_ = x[actDisableRaw-73]
|
||||
_ = x[actTrackCurrent-74]
|
||||
_ = x[actToggleInput-75]
|
||||
_ = x[actHideInput-76]
|
||||
_ = x[actShowInput-77]
|
||||
_ = x[actUntrackCurrent-78]
|
||||
_ = x[actDown-79]
|
||||
_ = x[actDownMatch-80]
|
||||
_ = x[actUp-81]
|
||||
_ = x[actUpMatch-82]
|
||||
_ = x[actPageUp-83]
|
||||
_ = x[actPageDown-84]
|
||||
_ = x[actPosition-85]
|
||||
_ = x[actHalfPageUp-86]
|
||||
_ = x[actHalfPageDown-87]
|
||||
_ = x[actOffsetUp-88]
|
||||
_ = x[actOffsetDown-89]
|
||||
_ = x[actOffsetMiddle-90]
|
||||
_ = x[actJump-91]
|
||||
_ = x[actJumpAccept-92]
|
||||
_ = x[actPrintQuery-93]
|
||||
_ = x[actRefreshPreview-94]
|
||||
_ = x[actReplaceQuery-95]
|
||||
_ = x[actToggleSort-96]
|
||||
_ = x[actShowPreview-97]
|
||||
_ = x[actHidePreview-98]
|
||||
_ = x[actTogglePreview-99]
|
||||
_ = x[actTogglePreviewWrap-100]
|
||||
_ = x[actTogglePreviewWrapWord-101]
|
||||
_ = x[actTransform-102]
|
||||
_ = x[actTransformBorderLabel-103]
|
||||
_ = x[actTransformGhost-104]
|
||||
_ = x[actTransformHeader-105]
|
||||
_ = x[actTransformHeaderLines-106]
|
||||
_ = x[actTransformFooter-107]
|
||||
_ = x[actTransformHeaderLabel-108]
|
||||
_ = x[actTransformFooterLabel-109]
|
||||
_ = x[actTransformInputLabel-110]
|
||||
_ = x[actTransformListLabel-111]
|
||||
_ = x[actTransformNth-112]
|
||||
_ = x[actTransformPointer-113]
|
||||
_ = x[actTransformPreviewLabel-114]
|
||||
_ = x[actTransformPrompt-115]
|
||||
_ = x[actTransformQuery-116]
|
||||
_ = x[actTransformSearch-117]
|
||||
_ = x[actTrigger-118]
|
||||
_ = x[actBgTransform-119]
|
||||
_ = x[actBgTransformBorderLabel-120]
|
||||
_ = x[actBgTransformGhost-121]
|
||||
_ = x[actBgTransformHeader-122]
|
||||
_ = x[actBgTransformHeaderLines-123]
|
||||
_ = x[actBgTransformFooter-124]
|
||||
_ = x[actBgTransformHeaderLabel-125]
|
||||
_ = x[actBgTransformFooterLabel-126]
|
||||
_ = x[actBgTransformInputLabel-127]
|
||||
_ = x[actBgTransformListLabel-128]
|
||||
_ = x[actBgTransformNth-129]
|
||||
_ = x[actBgTransformPointer-130]
|
||||
_ = x[actBgTransformPreviewLabel-131]
|
||||
_ = x[actBgTransformPrompt-132]
|
||||
_ = x[actBgTransformQuery-133]
|
||||
_ = x[actBgTransformSearch-134]
|
||||
_ = x[actBgCancel-135]
|
||||
_ = x[actSearch-136]
|
||||
_ = x[actPreview-137]
|
||||
_ = x[actPreviewTop-138]
|
||||
_ = x[actPreviewBottom-139]
|
||||
_ = x[actPreviewUp-140]
|
||||
_ = x[actPreviewDown-141]
|
||||
_ = x[actPreviewPageUp-142]
|
||||
_ = x[actPreviewPageDown-143]
|
||||
_ = x[actPreviewHalfPageUp-144]
|
||||
_ = x[actPreviewHalfPageDown-145]
|
||||
_ = x[actPrevHistory-146]
|
||||
_ = x[actPrevSelected-147]
|
||||
_ = x[actPrint-148]
|
||||
_ = x[actPut-149]
|
||||
_ = x[actNextHistory-150]
|
||||
_ = x[actNextSelected-151]
|
||||
_ = x[actExecute-152]
|
||||
_ = x[actExecuteSilent-153]
|
||||
_ = x[actExecuteMulti-154]
|
||||
_ = x[actSigStop-155]
|
||||
_ = x[actBest-156]
|
||||
_ = x[actFirst-157]
|
||||
_ = x[actLast-158]
|
||||
_ = x[actReload-159]
|
||||
_ = x[actReloadSync-160]
|
||||
_ = x[actDisableSearch-161]
|
||||
_ = x[actEnableSearch-162]
|
||||
_ = x[actSelect-163]
|
||||
_ = x[actDeselect-164]
|
||||
_ = x[actUnbind-165]
|
||||
_ = x[actRebind-166]
|
||||
_ = x[actToggleBind-167]
|
||||
_ = x[actBecome-168]
|
||||
_ = x[actShowHeader-169]
|
||||
_ = x[actHideHeader-170]
|
||||
_ = x[actBell-171]
|
||||
_ = x[actExclude-172]
|
||||
_ = x[actExcludeMulti-173]
|
||||
_ = x[actAsync-174]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
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}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 507, 529, 544, 558, 572, 585, 602, 610, 623, 639, 651, 659, 673, 687, 704, 715, 726, 740, 758, 775, 782, 801, 823, 835, 849, 858, 873, 885, 898, 909, 920, 932, 946, 967, 982, 995, 1012, 1030, 1046, 1058, 1070, 1083, 1098, 1112, 1124, 1136, 1153, 1160, 1172, 1177, 1187, 1196, 1207, 1218, 1231, 1246, 1257, 1270, 1285, 1292, 1305, 1318, 1335, 1350, 1363, 1377, 1391, 1407, 1427, 1451, 1463, 1486, 1503, 1521, 1544, 1562, 1585, 1608, 1630, 1651, 1666, 1685, 1709, 1727, 1744, 1762, 1772, 1786, 1811, 1830, 1850, 1875, 1895, 1920, 1945, 1969, 1992, 2009, 2030, 2056, 2076, 2095, 2115, 2126, 2135, 2145, 2158, 2174, 2186, 2200, 2216, 2234, 2254, 2276, 2290, 2305, 2313, 2319, 2333, 2348, 2358, 2374, 2389, 2399, 2406, 2414, 2421, 2430, 2443, 2459, 2474, 2483, 2494, 2503, 2512, 2525, 2534, 2547, 2560, 2567, 2577, 2592, 2600}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# 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.
|
||||
@@ -321,15 +321,22 @@ type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Cha
|
||||
|
||||
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
||||
byteArray := input.Bytes()[from:]
|
||||
// For case-insensitive search of a letter, search for both cases in one pass
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
idx := IndexByteTwo(byteArray, b, b-32)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
return from + idx
|
||||
}
|
||||
idx := bytes.IndexByte(byteArray, b)
|
||||
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 idx > 0 {
|
||||
byteArray = byteArray[:idx]
|
||||
}
|
||||
uidx := bytes.IndexByte(byteArray, b-32)
|
||||
if uidx >= 0 {
|
||||
idx = uidx
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
@@ -373,17 +380,14 @@ 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
|
||||
bu := b
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
bu = b - 32
|
||||
}
|
||||
scope := input.Bytes()[lastIdx:]
|
||||
if len(scope) > 1 {
|
||||
tail := scope[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
|
||||
for offset := len(scope) - 1; offset > 0; offset-- {
|
||||
if scope[offset] == b || scope[offset] == bu {
|
||||
return firstIdx, lastIdx + offset + 1
|
||||
}
|
||||
}
|
||||
return firstIdx, lastIdx + 1
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//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
|
||||
@@ -1,377 +0,0 @@
|
||||
#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
|
||||
@@ -1,17 +0,0 @@
|
||||
//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
|
||||
@@ -1,249 +0,0 @@
|
||||
#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
|
||||
@@ -1,33 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
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) }
|
||||
33
src/ansi.go
33
src/ansi.go
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
@@ -124,31 +123,31 @@ func toAnsiString(color tui.Color, offset int) string {
|
||||
return ret + ";"
|
||||
}
|
||||
|
||||
func isPrint(c uint8) bool {
|
||||
return '\x20' <= c && c <= '\x7e'
|
||||
}
|
||||
|
||||
func matchOperatingSystemCommand(s string, start int) int {
|
||||
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here after the first printable character
|
||||
//
|
||||
i := start // prefix matched in nextAnsiEscapeSequence()
|
||||
|
||||
// Find the terminator: BEL (\x07) or ESC (\x1b) for ST (\x1b\\)
|
||||
idx := algo.IndexByteTwo(stringBytes(s[i:]), '\x07', '\x1b')
|
||||
if idx < 0 {
|
||||
return -1
|
||||
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||
}
|
||||
i += idx
|
||||
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
if i < len(s) {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------------
|
||||
if s[:i+1] == "\x1b]8;;\x1b" {
|
||||
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
@@ -234,7 +233,7 @@ Loop:
|
||||
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ---------------
|
||||
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && s[i+j+1] >= '\x20' {
|
||||
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && isPrint(s[i+j+1]) {
|
||||
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
|
||||
return i, i + k
|
||||
}
|
||||
|
||||
33
src/cache.go
33
src/cache.go
@@ -2,13 +2,10 @@ package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// ChunkBitmap is a bitmap with one bit per item in a chunk.
|
||||
type ChunkBitmap [chunkBitWords]uint64
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]Result
|
||||
|
||||
// queryCache associates query strings to bitmaps of matching items
|
||||
type queryCache map[string]ChunkBitmap
|
||||
|
||||
// ChunkCache associates Chunk and query string to bitmaps
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
type ChunkCache struct {
|
||||
mutex sync.Mutex
|
||||
cache map[*Chunk]*queryCache
|
||||
@@ -33,9 +30,9 @@ func (cc *ChunkCache) retire(chunk ...*Chunk) {
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Add stores the bitmap for the given chunk and key
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, bitmap ChunkBitmap, matchCount int) {
|
||||
if len(key) == 0 || !chunk.IsFull() || matchCount > queryCacheMax {
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,11 +44,11 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, bitmap ChunkBitmap, matchCou
|
||||
cc.cache[chunk] = &queryCache{}
|
||||
qc = cc.cache[chunk]
|
||||
}
|
||||
(*qc)[key] = bitmap
|
||||
(*qc)[key] = list
|
||||
}
|
||||
|
||||
// Lookup returns the bitmap for the exact key
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) *ChunkBitmap {
|
||||
// Lookup is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
@@ -61,15 +58,15 @@ func (cc *ChunkCache) Lookup(chunk *Chunk, key string) *ChunkBitmap {
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if ok {
|
||||
if bm, ok := (*qc)[key]; ok {
|
||||
return &bm
|
||||
list, ok := (*qc)[key]
|
||||
if ok {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search finds the bitmap for the longest prefix or suffix of the key
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) *ChunkBitmap {
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
@@ -89,8 +86,8 @@ func (cc *ChunkCache) Search(chunk *Chunk, key string) *ChunkBitmap {
|
||||
prefix := key[:len(key)-idx]
|
||||
suffix := key[idx:]
|
||||
for _, substr := range [2]string{prefix, suffix} {
|
||||
if bm, found := (*qc)[substr]; found {
|
||||
return &bm
|
||||
if cached, found := (*qc)[substr]; found {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,34 +6,34 @@ func TestChunkCache(t *testing.T) {
|
||||
cache := NewChunkCache()
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &Chunk{count: chunkSize}
|
||||
bm1 := ChunkBitmap{1}
|
||||
bm2 := ChunkBitmap{1, 2}
|
||||
cache.Add(chunk1p, "foo", bm1, 1)
|
||||
cache.Add(chunk2p, "foo", bm1, 1)
|
||||
cache.Add(chunk2p, "bar", bm2, 2)
|
||||
items1 := []Result{{}}
|
||||
items2 := []Result{{}, {}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
||||
{ // chunk1 is not full
|
||||
cached := cache.Lookup(chunk1p, "foo")
|
||||
if cached != nil {
|
||||
t.Error("Cached disabled for non-full chunks", cached)
|
||||
t.Error("Cached disabled for non-empty chunks", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "foo")
|
||||
if cached == nil || cached[0] != 1 {
|
||||
t.Error("Expected bitmap cached", cached)
|
||||
if cached == nil || len(cached) != 1 {
|
||||
t.Error("Expected 1 item cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "bar")
|
||||
if cached == nil || cached[1] != 2 {
|
||||
t.Error("Expected bitmap cached", cached)
|
||||
if cached == nil || len(cached) != 2 {
|
||||
t.Error("Expected 2 items cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk1p, "foobar")
|
||||
if cached != nil {
|
||||
t.Error("Expected nil cached", cached)
|
||||
t.Error("Expected 0 item cached", cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,21 +99,6 @@ func (cl *ChunkList) Clear() {
|
||||
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
|
||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
@@ -34,18 +34,19 @@ const (
|
||||
maxBgProcessesPerAction = 3
|
||||
|
||||
// Matcher
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
|
||||
// Capacity of each chunk
|
||||
chunkSize int = 1024
|
||||
chunkBitWords = (chunkSize + 63) / 64
|
||||
chunkSize int = 1000
|
||||
|
||||
// Pre-allocated memory slices to minimize GC
|
||||
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
||||
slab32Size int = 2048 // 8KB * 32 = 256KB
|
||||
|
||||
// Do not cache results of low selectivity queries
|
||||
queryCacheMax int = chunkSize / 2
|
||||
queryCacheMax int = chunkSize / 5
|
||||
|
||||
// Not to cache mergers with large lists
|
||||
mergerCacheMax int = 100000
|
||||
|
||||
117
src/core.go
117
src/core.go
@@ -56,9 +56,6 @@ func Run(opts *Options) (int, error) {
|
||||
if opts.useTmux() {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
if opts.useZellij() {
|
||||
return runZellij(os.Args, opts)
|
||||
}
|
||||
|
||||
if needWinpty(opts) {
|
||||
return runWinpty(os.Args, opts)
|
||||
@@ -116,42 +113,6 @@ func Run(opts *Options) (int, error) {
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
// 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 {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
item.text, item.colors = ansiProcessor(data)
|
||||
@@ -160,13 +121,38 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
nthTransformer = opts.WithNth(opts.Delimiter)
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if nthTransformer == nil {
|
||||
item.text, item.colors = ansiProcessor(data)
|
||||
} else {
|
||||
transformItem(item, data, nthTransformer, itemIndex)
|
||||
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 := nthTransformer(tokens, itemIndex)
|
||||
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.origText = &data
|
||||
itemIndex++
|
||||
@@ -198,13 +184,11 @@ func Run(opts *Options) (int, error) {
|
||||
// Reader
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
|
||||
var reader *Reader
|
||||
var ingestionStart time.Time
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
|
||||
ingestionStart = time.Now()
|
||||
readyChan := make(chan bool)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||
<-readyChan
|
||||
@@ -288,7 +272,6 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
ingestionTime := time.Since(ingestionStart)
|
||||
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
@@ -322,14 +305,13 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
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%%) ingestion: %.2fms\n",
|
||||
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,
|
||||
float64(ingestionTime.Microseconds())/1000)
|
||||
totalItems, matchCount, selectivity)
|
||||
return ExitOk, nil
|
||||
}
|
||||
|
||||
@@ -472,16 +454,13 @@ func Run(opts *Options) (int, error) {
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
if !useSnapshot || evt == EvtReadFin {
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *commandSpec
|
||||
var environ []string
|
||||
var changed bool
|
||||
headerLinesChanged := false
|
||||
withNthChanged := false
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
@@ -508,34 +487,6 @@ func Run(opts *Options) (int, error) {
|
||||
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 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
@@ -579,8 +530,6 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
terminal.UpdateHeader(nil)
|
||||
}
|
||||
} else if withNthChanged && headerLines > 0 {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
}
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -46,8 +45,6 @@ type Matcher struct {
|
||||
sortBuf [][]Result
|
||||
mergerCache map[string]MatchResult
|
||||
revision revision
|
||||
scanMutex sync.Mutex
|
||||
cancelScan *util.AtomicBool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -58,7 +55,7 @@ const (
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
|
||||
partitions := runtime.NumCPU()
|
||||
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
if threads > 0 {
|
||||
partitions = threads
|
||||
}
|
||||
@@ -73,8 +70,7 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
sortBuf: make([][]Result, partitions),
|
||||
mergerCache: make(map[string]MatchResult),
|
||||
revision: revision,
|
||||
cancelScan: util.NewAtomicBool(false)}
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
@@ -134,9 +130,7 @@ func (m *Matcher) Loop() {
|
||||
}
|
||||
|
||||
if result.merger == nil {
|
||||
m.scanMutex.Lock()
|
||||
result = m.scan(request)
|
||||
m.scanMutex.Unlock()
|
||||
}
|
||||
|
||||
if !result.cancelled {
|
||||
@@ -149,6 +143,27 @@ func (m *Matcher) Loop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
partitions := m.partitions
|
||||
perSlice := len(chunks) / partitions
|
||||
|
||||
if perSlice == 0 {
|
||||
partitions = len(chunks)
|
||||
perSlice = 1
|
||||
}
|
||||
|
||||
slices := make([][]*Chunk, partitions)
|
||||
for i := 0; i < partitions; i++ {
|
||||
start := i * perSlice
|
||||
end := start + perSlice
|
||||
if i == partitions-1 {
|
||||
end = len(chunks)
|
||||
}
|
||||
slices[i] = chunks[start:end]
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
type partialResult struct {
|
||||
index int
|
||||
matches []Result
|
||||
@@ -172,37 +187,39 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
numWorkers := min(m.partitions, numChunks)
|
||||
var nextChunk atomic.Int32
|
||||
resultChan := make(chan partialResult, numWorkers)
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
numSlices := len(slices)
|
||||
resultChan := make(chan partialResult, numSlices)
|
||||
countChan := make(chan int, numChunks)
|
||||
waitGroup := sync.WaitGroup{}
|
||||
|
||||
for idx := range numWorkers {
|
||||
for idx, chunks := range slices {
|
||||
waitGroup.Add(1)
|
||||
if m.slab[idx] == nil {
|
||||
m.slab[idx] = util.MakeSlab(slab16Size, slab32Size)
|
||||
}
|
||||
go func(idx int, slab *util.Slab) {
|
||||
defer waitGroup.Done()
|
||||
var matches []Result
|
||||
for {
|
||||
ci := int(nextChunk.Add(1)) - 1
|
||||
if ci >= numChunks {
|
||||
break
|
||||
}
|
||||
chunkMatches := request.pattern.Match(request.chunks[ci], slab)
|
||||
matches = append(matches, chunkMatches...)
|
||||
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
||||
defer func() { waitGroup.Done() }()
|
||||
count := 0
|
||||
allMatches := make([][]Result, len(chunks))
|
||||
for idx, chunk := range chunks {
|
||||
matches := request.pattern.Match(chunk, slab)
|
||||
allMatches[idx] = matches
|
||||
count += len(matches)
|
||||
if cancelled.Get() {
|
||||
return
|
||||
}
|
||||
countChan <- len(chunkMatches)
|
||||
countChan <- len(matches)
|
||||
}
|
||||
sliceMatches := make([]Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort && request.pattern.sortable {
|
||||
m.sortBuf[idx] = radixSortResults(matches, m.tac, m.sortBuf[idx])
|
||||
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx])
|
||||
}
|
||||
resultChan <- partialResult{idx, matches}
|
||||
}(idx, m.slab[idx])
|
||||
resultChan <- partialResult{idx, sliceMatches}
|
||||
}(idx, m.slab[idx], chunks)
|
||||
}
|
||||
|
||||
wait := func() bool {
|
||||
@@ -221,7 +238,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
break
|
||||
}
|
||||
|
||||
if m.cancelScan.Get() || m.reqBox.Peek(reqReset) {
|
||||
if m.reqBox.Peek(reqReset) {
|
||||
return MatchResult{nil, nil, wait()}
|
||||
}
|
||||
|
||||
@@ -230,8 +247,8 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]Result, numWorkers)
|
||||
for range numWorkers {
|
||||
partialResults := make([][]Result, numSlices)
|
||||
for range slices {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
@@ -252,20 +269,6 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
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() {
|
||||
m.reqBox.Set(reqQuit, nil)
|
||||
}
|
||||
|
||||
@@ -136,7 +136,14 @@ func (mg *Merger) Get(idx int) Result {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
return mg.mergedGet(idx)
|
||||
for _, list := range mg.lists {
|
||||
numItems := len(list)
|
||||
if idx < numItems {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= numItems
|
||||
}
|
||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||
}
|
||||
|
||||
func (mg *Merger) ToMap() map[int32]Result {
|
||||
@@ -164,7 +171,7 @@ func (mg *Merger) mergedGet(idx int) Result {
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor]
|
||||
if minIdx < 0 || mg.sorted && compareRanks(rank, minRank, mg.tac) || !mg.sorted && rank.item.Index() < minRank.item.Index() {
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
}
|
||||
|
||||
@@ -54,25 +54,10 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
}
|
||||
|
||||
func TestMergerUnsorted(t *testing.T) {
|
||||
lists, _ := buildLists(false)
|
||||
|
||||
// Sort each list by index to simulate real worker behavior
|
||||
// (workers process chunks in ascending order via nextChunk.Add(1))
|
||||
for _, list := range lists {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].item.Index() < list[j].item.Index()
|
||||
})
|
||||
}
|
||||
items := []Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].item.Index() < items[j].item.Index()
|
||||
})
|
||||
lists, items := buildLists(false)
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: items in ascending index order
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := range cnt {
|
||||
|
||||
@@ -66,7 +66,7 @@ Usage: fzf [options]
|
||||
--no-bold Do not use bold text
|
||||
|
||||
DISPLAY MODE
|
||||
--height=[~][-]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calculated as the terminal height
|
||||
minus the given value.
|
||||
@@ -75,10 +75,9 @@ Usage: fzf [options]
|
||||
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
|
||||
Add '+' to automatically increase the value
|
||||
according to the other layout options (default: 10+).
|
||||
--popup[=OPTS] Start fzf in a popup window (requires tmux 3.3+ or Zellij 0.44+)
|
||||
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
|
||||
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
[,border-native] (default: center,50%)
|
||||
--tmux[=OPTS] Alias for --popup
|
||||
|
||||
LAYOUT
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
@@ -102,7 +101,6 @@ Usage: fzf [options]
|
||||
--no-multi-line Disable multi-line display of items when using --read0
|
||||
--raw Enable raw mode (show non-matching items)
|
||||
--track Track the current selection when the result is updated
|
||||
--id-nth=N[,..] Define item identity fields for cross-reload operations
|
||||
--tac Reverse the order of the input
|
||||
--gap[=N] Render empty lines between each item
|
||||
--gap-line[=STR] Draw horizontal line on each gap using the string
|
||||
@@ -418,7 +416,7 @@ func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
|
||||
var err error
|
||||
opts := defaultTmuxOptions(index)
|
||||
tokens := splitRegexp.Split(arg, -1)
|
||||
errorToReturn := errors.New("invalid popup option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]])")
|
||||
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]])")
|
||||
if len(tokens) == 0 || len(tokens) > 4 {
|
||||
return nil, errorToReturn
|
||||
}
|
||||
@@ -590,13 +588,11 @@ type Options struct {
|
||||
FreezeLeft int
|
||||
FreezeRight int
|
||||
WithNth func(Delimiter) func([]Token, int32) string
|
||||
WithNthExpr string
|
||||
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Raw bool
|
||||
Track trackOption
|
||||
IdNth []Range
|
||||
Tac bool
|
||||
Tail int
|
||||
Criteria []criterion
|
||||
@@ -870,7 +866,7 @@ func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, er
|
||||
nth []Range
|
||||
}
|
||||
|
||||
parts := make([]NthParts, 0, len(indexes))
|
||||
parts := make([]NthParts, len(indexes))
|
||||
idx := 0
|
||||
for _, index := range indexes {
|
||||
if idx < index[0] {
|
||||
@@ -1613,7 +1609,7 @@ func parseWalkerOpts(str string) (walkerOpts, error) {
|
||||
}
|
||||
|
||||
var (
|
||||
argActionRegexp *regexp.Regexp
|
||||
executeRegexp *regexp.Regexp
|
||||
splitRegexp *regexp.Regexp
|
||||
actionNameRegexp *regexp.Regexp
|
||||
)
|
||||
@@ -1632,8 +1628,8 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
argActionRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|with-nth|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header-lines|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1642,7 +1638,7 @@ func maskActionContents(action string) string {
|
||||
masked := ""
|
||||
Loop:
|
||||
for len(action) > 0 {
|
||||
loc := argActionRegexp.FindStringIndex(action)
|
||||
loc := executeRegexp.FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
break
|
||||
@@ -1697,7 +1693,7 @@ Loop:
|
||||
}
|
||||
|
||||
func parseSingleActionList(str string) ([]*action, error) {
|
||||
// We prepend a colon to satisfy argActionRegexp and remove it later
|
||||
// We prepend a colon to satisfy executeRegexp and remove it later
|
||||
masked := maskActionContents(":" + str)[1:]
|
||||
return parseActionList(masked, str, []*action{}, false)
|
||||
}
|
||||
@@ -2076,8 +2072,6 @@ func isExecuteAction(str string) actionType {
|
||||
return actChangeMulti
|
||||
case "change-nth":
|
||||
return actChangeNth
|
||||
case "change-with-nth":
|
||||
return actChangeWithNth
|
||||
case "pos":
|
||||
return actPosition
|
||||
case "execute":
|
||||
@@ -2114,8 +2108,6 @@ func isExecuteAction(str string) actionType {
|
||||
return actTransformGhost
|
||||
case "transform-nth":
|
||||
return actTransformNth
|
||||
case "transform-with-nth":
|
||||
return actTransformWithNth
|
||||
case "transform-pointer":
|
||||
return actTransformPointer
|
||||
case "transform-prompt":
|
||||
@@ -2148,8 +2140,6 @@ func isExecuteAction(str string) actionType {
|
||||
return actBgTransformGhost
|
||||
case "bg-transform-nth":
|
||||
return actBgTransformNth
|
||||
case "bg-transform-with-nth":
|
||||
return actBgTransformWithNth
|
||||
case "bg-transform-pointer":
|
||||
return actBgTransformPointer
|
||||
case "bg-transform-prompt":
|
||||
@@ -2222,6 +2212,9 @@ func parseHeight(str string, index int) (heightSpec, error) {
|
||||
str = str[1:]
|
||||
}
|
||||
if strings.HasPrefix(str, "-") {
|
||||
if heightSpec.auto {
|
||||
return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
|
||||
}
|
||||
heightSpec.inverse = true
|
||||
str = str[1:]
|
||||
}
|
||||
@@ -2634,7 +2627,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.Version = true
|
||||
case "--no-winpty":
|
||||
opts.NoWinpty = true
|
||||
case "--tmux", "--popup":
|
||||
case "--tmux":
|
||||
given, str := optionalNextString()
|
||||
if given {
|
||||
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
|
||||
@@ -2643,7 +2636,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
} else {
|
||||
opts.Tmux = defaultTmuxOptions(index)
|
||||
}
|
||||
case "--no-tmux", "--no-popup":
|
||||
case "--no-tmux":
|
||||
opts.Tmux = nil
|
||||
case "--tty-default":
|
||||
if opts.TtyDefault, err = nextString("tty device name required"); err != nil {
|
||||
@@ -2788,7 +2781,6 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.WithNthExpr = str
|
||||
case "--accept-nth":
|
||||
str, err := nextString("nth expression required")
|
||||
if err != nil {
|
||||
@@ -2811,16 +2803,6 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.Track = trackEnabled
|
||||
case "--no-track":
|
||||
opts.Track = trackDisabled
|
||||
case "--id-nth":
|
||||
str, err := nextString("nth expression required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.IdNth, err = splitNth(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-id-nth":
|
||||
opts.IdNth = nil
|
||||
case "--tac":
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
@@ -3149,7 +3131,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
}
|
||||
opts.PreviewWrapSign = &str
|
||||
case "--height":
|
||||
str, err := nextString("height required: [~][-]HEIGHT[%]")
|
||||
str, err := nextString("height required: [~]HEIGHT[%]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -3591,7 +3573,7 @@ func validateOptions(opts *Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Height.auto && (opts.Tmux == nil || opts.Tmux.index < opts.Height.index) {
|
||||
if opts.Height.auto {
|
||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
||||
if s.percent {
|
||||
return errors.New("adaptive height is not compatible with top/bottom percent margin")
|
||||
@@ -3625,10 +3607,6 @@ func (opts *Options) useTmux() bool {
|
||||
return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index
|
||||
}
|
||||
|
||||
func (opts *Options) useZellij() bool {
|
||||
return opts.Tmux != nil && len(os.Getenv("ZELLIJ")) > 0 && opts.Tmux.index >= opts.Height.index
|
||||
}
|
||||
|
||||
func (opts *Options) noSeparatorLine() bool {
|
||||
if opts.Inputless {
|
||||
return true
|
||||
|
||||
103
src/pattern.go
103
src/pattern.go
@@ -61,7 +61,7 @@ type Pattern struct {
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision revision
|
||||
procFun [6]algo.Algo
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
startIndex int32
|
||||
@@ -150,7 +150,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
startIndex: startIndex,
|
||||
}
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
|
||||
@@ -300,87 +300,104 @@ func (p *Pattern) CacheKey() string {
|
||||
|
||||
// Match returns the list of matches Items in the given Chunk
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
|
||||
// Bitmap cache: exact match or prefix/suffix
|
||||
var cachedBitmap *ChunkBitmap
|
||||
if p.cacheable {
|
||||
cachedBitmap = p.cache.Lookup(chunk, cacheKey)
|
||||
}
|
||||
if cachedBitmap == nil {
|
||||
cachedBitmap = p.cache.Search(chunk, cacheKey)
|
||||
if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
matches, bitmap := p.matchChunk(chunk, cachedBitmap, slab)
|
||||
// Prefix/suffix cache
|
||||
space := p.cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
|
||||
if p.cacheable {
|
||||
p.cache.Add(chunk, cacheKey, bitmap, len(matches))
|
||||
p.cache.Add(chunk, cacheKey, matches)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, cachedBitmap *ChunkBitmap, slab *util.Slab) ([]Result, ChunkBitmap) {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
var bitmap ChunkBitmap
|
||||
|
||||
// Skip header items in chunks that contain them
|
||||
startIdx := 0
|
||||
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
|
||||
startIdx = int(p.startIndex - chunk.items[0].Index())
|
||||
if startIdx >= chunk.count {
|
||||
return matches, bitmap
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
hasCachedBitmap := cachedBitmap != nil
|
||||
|
||||
// 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
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
continue
|
||||
if space == nil {
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&chunk.items[idx].text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
&chunk.items[idx], res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
}
|
||||
}
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&chunk.items[idx].text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
&chunk.items[idx], res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
} else {
|
||||
for _, result := range space {
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&result.item.text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
result.item, res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
}
|
||||
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, match)
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match.item != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns the match result if the Item is a match.
|
||||
|
||||
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
@@ -138,7 +137,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
origText: &origBytes,
|
||||
transformed: &transformed{pattern.revision, trans}}
|
||||
pattern.extended = extended
|
||||
matches, _ := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||
reflect.DeepEqual((*matches[0].item.transformed).tokens, trans)) {
|
||||
@@ -200,119 +199,3 @@ func TestCacheable(t *testing.T) {
|
||||
test(false, "foo 'bar", "foo", false)
|
||||
test(false, "foo !bar", "foo", false)
|
||||
}
|
||||
|
||||
func buildChunks(numChunks int) []*Chunk {
|
||||
chunks := make([]*Chunk, numChunks)
|
||||
words := []string{
|
||||
"src/main/java/com/example/service/UserService.java",
|
||||
"src/test/java/com/example/service/UserServiceTest.java",
|
||||
"docs/api/reference/endpoints.md",
|
||||
"lib/internal/utils/string_helper.go",
|
||||
"pkg/server/http/handler/auth.go",
|
||||
"build/output/release/app.exe",
|
||||
"config/production/database.yml",
|
||||
"scripts/deploy/kubernetes/setup.sh",
|
||||
"vendor/github.com/junegunn/fzf/src/core.go",
|
||||
"node_modules/.cache/babel/transform.js",
|
||||
}
|
||||
for ci := range numChunks {
|
||||
chunks[ci] = &Chunk{count: chunkSize}
|
||||
for i := range chunkSize {
|
||||
text := words[(ci*chunkSize+i)%len(words)]
|
||||
chunks[ci].items[i] = Item{text: util.ToChars([]byte(text))}
|
||||
chunks[ci].items[i].text.Index = int32(ci*chunkSize + i)
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func buildPatternWith(cache *ChunkCache, runes []rune) *Pattern {
|
||||
return BuildPattern(cache, make(map[string]*Pattern),
|
||||
true, algo.FuzzyMatchV2, true, CaseSmart, false, true,
|
||||
false, true, []Range{}, Delimiter{}, revision{}, runes, nil, 0)
|
||||
}
|
||||
|
||||
func TestBitmapCacheBenefit(t *testing.T) {
|
||||
numChunks := 100
|
||||
chunks := buildChunks(numChunks)
|
||||
queries := []string{"s", "se", "ser", "serv", "servi"}
|
||||
|
||||
// 1. Run all queries with shared cache (simulates incremental typing)
|
||||
cache := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. GC and measure memory with cache populated
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
var memWith runtime.MemStats
|
||||
runtime.ReadMemStats(&memWith)
|
||||
|
||||
// 3. Clear cache, GC, measure again
|
||||
cache.Clear()
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
var memWithout runtime.MemStats
|
||||
runtime.ReadMemStats(&memWithout)
|
||||
|
||||
cacheMem := int64(memWith.Alloc) - int64(memWithout.Alloc)
|
||||
t.Logf("Chunks: %d, Queries: %d", numChunks, len(queries))
|
||||
t.Logf("Cache memory: %d bytes (%.1f KB)", cacheMem, float64(cacheMem)/1024)
|
||||
t.Logf("Per-chunk-per-query: %.0f bytes", float64(cacheMem)/float64(numChunks*len(queries)))
|
||||
|
||||
// 4. Verify correctness: cached vs uncached produce same results
|
||||
cache2 := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache2, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
for _, q := range queries {
|
||||
patCached := buildPatternWith(cache2, []rune(q))
|
||||
patFresh := buildPatternWith(NewChunkCache(), []rune(q))
|
||||
var countCached, countFresh int
|
||||
for _, chunk := range chunks {
|
||||
countCached += len(patCached.Match(chunk, slab))
|
||||
countFresh += len(patFresh.Match(chunk, slab))
|
||||
}
|
||||
if countCached != countFresh {
|
||||
t.Errorf("query=%q: cached=%d, fresh=%d", q, countCached, countFresh)
|
||||
}
|
||||
t.Logf("query=%q: matches=%d", q, countCached)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWithCache(b *testing.B) {
|
||||
numChunks := 100
|
||||
chunks := buildChunks(numChunks)
|
||||
queries := []string{"s", "se", "ser", "serv", "servi"}
|
||||
|
||||
b.Run("cached", func(b *testing.B) {
|
||||
for range b.N {
|
||||
cache := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("uncached", func(b *testing.B) {
|
||||
for range b.N {
|
||||
for _, q := range queries {
|
||||
cache := NewChunkCache()
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio cpath dpath wpath rpath inet fattr unix tty proc exec")
|
||||
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
||||
26
src/proxy.go
26
src/proxy.go
@@ -23,32 +23,6 @@ func escapeSingleQuote(str string) string {
|
||||
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func popupArgStr(args []string, opts *Options) (string, string) {
|
||||
fzf, rest := args[0], args[1:]
|
||||
args = []string{"--bind=ctrl-z:ignore"}
|
||||
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range append(args, rest...) {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-popup --no-height`
|
||||
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
return argStr, dir
|
||||
}
|
||||
|
||||
func fifo(name string) (string, error) {
|
||||
ns := time.Now().UnixNano()
|
||||
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
|
||||
|
||||
@@ -274,24 +274,6 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
|
||||
// When following symlinks, precompute the absolute real paths of walker
|
||||
// roots so we can skip symlinks that point to an ancestor. fastwalk's
|
||||
// built-in loop detection (shouldTraverse) catches loops on the second
|
||||
// pass, but a single pass through a symlink like z: -> / already
|
||||
// traverses the entire root filesystem, causing severe resource
|
||||
// exhaustion. Skipping ancestor symlinks prevents this entirely.
|
||||
var absRoots []string
|
||||
if opts.follow {
|
||||
for _, root := range roots {
|
||||
if real, err := filepath.EvalSymlinks(root); err == nil {
|
||||
if abs, err := filepath.Abs(real); err == nil {
|
||||
absRoots = append(absRoots, filepath.Clean(abs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignoresBase := []string{}
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
@@ -325,24 +307,6 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
if isDirSymlink && !opts.follow {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Skip symlinks whose target is an ancestor of (or equal to)
|
||||
// any walker root. Following such symlinks would traverse a
|
||||
// superset of the tree we're already walking.
|
||||
if isDirSymlink && len(absRoots) > 0 {
|
||||
if target, err := filepath.EvalSymlinks(path); err == nil {
|
||||
if abs, err := filepath.Abs(target); err == nil {
|
||||
abs = filepath.Clean(abs)
|
||||
if abs == string(os.PathSeparator) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, absRoot := range absRoots {
|
||||
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
isDir := de.IsDir() || isDirSymlink
|
||||
if isDir {
|
||||
base := filepath.Base(path)
|
||||
|
||||
@@ -128,7 +128,7 @@ func minRank() Result {
|
||||
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, nthOverlay 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, hidden bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI codes
|
||||
@@ -213,10 +213,6 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
}
|
||||
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
|
||||
add := func(idx int) {
|
||||
if curr.fbg >= 0 {
|
||||
@@ -230,7 +226,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
if curr.match {
|
||||
var color tui.ColorPair
|
||||
if curr.nth {
|
||||
color = nthBase.Merge(colMatch)
|
||||
color = colBase.WithAttr(attrNth).Merge(colMatch)
|
||||
} else {
|
||||
color = colBase.Merge(colMatch)
|
||||
}
|
||||
@@ -250,7 +246,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||
color = origColor
|
||||
if curr.nth {
|
||||
color = color.WithAttr((attrNth &^ tui.AttrRegular).Merge(nthOverlay))
|
||||
color = color.WithAttr(attrNth &^ tui.AttrRegular)
|
||||
}
|
||||
} else {
|
||||
color = origColor.MergeNonDefault(color)
|
||||
@@ -262,7 +258,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
ansi := itemColors[curr.index]
|
||||
base := colBase
|
||||
if curr.nth {
|
||||
base = nthBase
|
||||
base = base.WithAttr(attrNth)
|
||||
}
|
||||
if hidden {
|
||||
base = base.WithFg(theme.Nomatch)
|
||||
@@ -274,7 +270,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
} else {
|
||||
color := nthBase
|
||||
color := colBase.WithAttr(attrNth)
|
||||
if hidden {
|
||||
color = color.WithFg(theme.Nomatch)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, 0, false)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
@@ -159,7 +159,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, 0, false)
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
|
||||
|
||||
// [{[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}}
|
||||
@@ -182,37 +182,6 @@ func TestColorOffset(t *testing.T) {
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||
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) {
|
||||
|
||||
@@ -122,12 +122,13 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
|
||||
321
src/terminal.go
321
src/terminal.go
@@ -216,9 +216,8 @@ const (
|
||||
)
|
||||
|
||||
type StatusItem struct {
|
||||
Index int `json:"index"`
|
||||
Text string `json:"text"`
|
||||
Positions []int `json:"positions,omitempty"`
|
||||
Index int `json:"index"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
@@ -315,12 +314,6 @@ type Terminal struct {
|
||||
sort bool
|
||||
toggleSort bool
|
||||
track trackOption
|
||||
idNth []Range
|
||||
trackKey string
|
||||
trackBlocked bool
|
||||
trackSync bool
|
||||
trackKeyCache map[int32]bool
|
||||
pendingSelections map[string]selectedItem
|
||||
targetIndex int32
|
||||
delimiter Delimiter
|
||||
expect map[tui.Event]string
|
||||
@@ -347,9 +340,6 @@ type Terminal struct {
|
||||
nthAttr tui.Attr
|
||||
nth []Range
|
||||
nthCurrent []Range
|
||||
withNthDefault string
|
||||
withNthExpr string
|
||||
withNthEnabled bool
|
||||
acceptNth func([]Token, int32) string
|
||||
tabstop int
|
||||
margin [4]sizeSpec
|
||||
@@ -394,8 +384,6 @@ type Terminal struct {
|
||||
hasLoadActions bool
|
||||
hasResizeActions bool
|
||||
triggerLoad bool
|
||||
pendingReqList bool
|
||||
filterSelection bool
|
||||
reading bool
|
||||
running *util.AtomicBool
|
||||
failed *string
|
||||
@@ -563,7 +551,6 @@ const (
|
||||
actChangeListLabel
|
||||
actChangeMulti
|
||||
actChangeNth
|
||||
actChangeWithNth
|
||||
actChangePointer
|
||||
actChangePreview
|
||||
actChangePreviewLabel
|
||||
@@ -649,7 +636,6 @@ const (
|
||||
actTransformInputLabel
|
||||
actTransformListLabel
|
||||
actTransformNth
|
||||
actTransformWithNth
|
||||
actTransformPointer
|
||||
actTransformPreviewLabel
|
||||
actTransformPrompt
|
||||
@@ -669,7 +655,6 @@ const (
|
||||
actBgTransformInputLabel
|
||||
actBgTransformListLabel
|
||||
actBgTransformNth
|
||||
actBgTransformWithNth
|
||||
actBgTransformPointer
|
||||
actBgTransformPreviewLabel
|
||||
actBgTransformPrompt
|
||||
@@ -736,7 +721,6 @@ func processExecution(action actionType) bool {
|
||||
actTransformInputLabel,
|
||||
actTransformListLabel,
|
||||
actTransformNth,
|
||||
actTransformWithNth,
|
||||
actTransformPointer,
|
||||
actTransformPreviewLabel,
|
||||
actTransformPrompt,
|
||||
@@ -753,7 +737,6 @@ func processExecution(action actionType) bool {
|
||||
actBgTransformInputLabel,
|
||||
actBgTransformListLabel,
|
||||
actBgTransformNth,
|
||||
actBgTransformWithNth,
|
||||
actBgTransformPointer,
|
||||
actBgTransformPreviewLabel,
|
||||
actBgTransformPrompt,
|
||||
@@ -783,15 +766,10 @@ type placeholderFlags struct {
|
||||
raw bool
|
||||
}
|
||||
|
||||
type withNthSpec struct {
|
||||
fn func([]Token, int32) string // nil = clear (restore original)
|
||||
}
|
||||
|
||||
type searchRequest struct {
|
||||
sort bool
|
||||
sync bool
|
||||
nth *[]Range
|
||||
withNth *withNthSpec
|
||||
headerLines *int
|
||||
command *commandSpec
|
||||
environ []string
|
||||
@@ -1051,7 +1029,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
sort: opts.Sort > 0,
|
||||
toggleSort: opts.ToggleSort,
|
||||
track: opts.Track,
|
||||
idNth: opts.IdNth,
|
||||
targetIndex: minItem.Index(),
|
||||
delimiter: opts.Delimiter,
|
||||
expect: opts.Expect,
|
||||
@@ -1103,9 +1080,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
nthAttr: opts.Theme.Nth.Attr,
|
||||
nth: opts.Nth,
|
||||
nthCurrent: opts.Nth,
|
||||
withNthDefault: opts.WithNthExpr,
|
||||
withNthExpr: opts.WithNthExpr,
|
||||
withNthEnabled: opts.WithNth != nil,
|
||||
tabstop: opts.Tabstop,
|
||||
raw: opts.Raw,
|
||||
hasStartActions: false,
|
||||
@@ -1377,9 +1351,6 @@ func (t *Terminal) environImpl(forPreview bool) []string {
|
||||
if len(t.nthCurrent) > 0 {
|
||||
env = append(env, "FZF_NTH="+RangesToString(t.nthCurrent))
|
||||
}
|
||||
if len(t.withNthExpr) > 0 {
|
||||
env = append(env, "FZF_WITH_NTH="+t.withNthExpr)
|
||||
}
|
||||
if t.raw {
|
||||
val := "0"
|
||||
if t.isCurrentItemMatch() {
|
||||
@@ -1563,7 +1534,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
|
||||
printFn := func(window tui.Window, limit int) {
|
||||
if offsets == nil {
|
||||
// tui.Col* are not initialized until renderer.Init()
|
||||
offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, 0, false)
|
||||
offsets = result.colorOffsets(nil, nil, t.theme, *color, *color, t.nthAttr, false)
|
||||
}
|
||||
for limit > 0 {
|
||||
if length > limit {
|
||||
@@ -1626,7 +1597,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
||||
return 1
|
||||
}
|
||||
t.printHighlighted(
|
||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil, 0)
|
||||
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, false, line, line, true, preTask, nil)
|
||||
})
|
||||
t.wrap = wrap
|
||||
}
|
||||
@@ -1749,17 +1720,6 @@ func (t *Terminal) Input() (bool, []rune) {
|
||||
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
|
||||
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
|
||||
t.mutex.Lock()
|
||||
@@ -1859,14 +1819,7 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
||||
}
|
||||
if t.revision != newRevision {
|
||||
if !t.revision.compatible(newRevision) {
|
||||
// Reloaded: capture selection keys for restoration, then clear (reload-sync only)
|
||||
if t.trackSync && len(t.idNth) > 0 && t.multi > 0 && len(t.selected) > 0 {
|
||||
t.pendingSelections = make(map[string]selectedItem, len(t.selected))
|
||||
for _, sel := range t.selected {
|
||||
key := t.trackKeyFor(sel.item, t.idNth)
|
||||
t.pendingSelections[key] = sel
|
||||
}
|
||||
}
|
||||
// Reloaded: clear selection
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.clearNumLinesCache()
|
||||
} else {
|
||||
@@ -1889,54 +1842,12 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
||||
}
|
||||
t.revision = newRevision
|
||||
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 {
|
||||
t.triggerLoad = false
|
||||
t.pendingReqList = true
|
||||
t.eventChan <- tui.Load.AsEvent()
|
||||
}
|
||||
// Search for the tracked item by nth key
|
||||
// - reload (async): search eagerly, unblock as soon as match is found
|
||||
// - reload-sync: wait until stream is complete before searching
|
||||
trackWasBlocked := t.trackBlocked
|
||||
if len(t.trackKey) > 0 && (!t.trackSync || !t.reading) {
|
||||
found := false
|
||||
for i := 0; i < t.merger.Length(); i++ {
|
||||
item := t.merger.Get(i).item
|
||||
idx := item.Index()
|
||||
match, ok := t.trackKeyCache[idx]
|
||||
if !ok {
|
||||
match = t.trackKeyFor(item, t.idNth) == t.trackKey
|
||||
t.trackKeyCache[idx] = match
|
||||
}
|
||||
if match {
|
||||
t.cy = i
|
||||
if t.track.Current() {
|
||||
t.track.index = idx
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found || !t.reading {
|
||||
t.unblockTrack()
|
||||
}
|
||||
} else if prevIndex >= 0 {
|
||||
if prevIndex >= 0 {
|
||||
pos := t.cy - t.offset
|
||||
count := t.merger.Length()
|
||||
i := t.merger.FindIndex(prevIndex)
|
||||
@@ -1952,25 +1863,12 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
||||
t.cy = count - min(count, t.maxItems()) + pos
|
||||
}
|
||||
}
|
||||
// Restore selections by id-nth key after reload completes
|
||||
if !t.reading && t.pendingSelections != nil {
|
||||
for i := 0; i < t.merger.Length() && len(t.pendingSelections) > 0; i++ {
|
||||
item := t.merger.Get(i).item
|
||||
key := t.trackKeyFor(item, t.idNth)
|
||||
if sel, found := t.pendingSelections[key]; found {
|
||||
t.selected[item.Index()] = selectedItem{sel.at, item}
|
||||
delete(t.pendingSelections, key)
|
||||
}
|
||||
}
|
||||
t.pendingSelections = nil
|
||||
}
|
||||
needActivation := false
|
||||
if !t.reading {
|
||||
switch t.resultMerger.Length() {
|
||||
case 0:
|
||||
zero := tui.Zero.AsEvent()
|
||||
if _, prs := t.keymap[zero]; prs {
|
||||
t.pendingReqList = true
|
||||
t.eventChan <- zero
|
||||
}
|
||||
// --sync, only 'focus' is bound, but no items to focus
|
||||
@@ -1978,26 +1876,16 @@ func (t *Terminal) UpdateList(result MatchResult) {
|
||||
case 1:
|
||||
one := tui.One.AsEvent()
|
||||
if _, prs := t.keymap[one]; prs {
|
||||
t.pendingReqList = true
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
}
|
||||
if t.hasResultActions {
|
||||
t.pendingReqList = true
|
||||
t.eventChan <- tui.Result.AsEvent()
|
||||
}
|
||||
updateList := !t.trackBlocked && !t.pendingReqList
|
||||
updatePrompt := trackWasBlocked && !t.trackBlocked
|
||||
t.mutex.Unlock()
|
||||
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
if updateList {
|
||||
t.reqBox.Set(reqList, nil)
|
||||
}
|
||||
if updatePrompt {
|
||||
t.reqBox.Set(reqPrompt, nil)
|
||||
}
|
||||
t.reqBox.Set(reqList, nil)
|
||||
if needActivation {
|
||||
t.reqBox.Set(reqActivate, nil)
|
||||
}
|
||||
@@ -2243,7 +2131,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
|
||||
width := screenWidth - marginInt[1] - marginInt[3]
|
||||
height := screenHeight - marginInt[0] - marginInt[2]
|
||||
|
||||
t.prevLines = make([]itemLine, max(1, screenHeight))
|
||||
t.prevLines = make([]itemLine, screenHeight)
|
||||
if t.border != nil && redrawBorder {
|
||||
t.border = nil
|
||||
}
|
||||
@@ -2946,8 +2834,6 @@ func (t *Terminal) printPrompt() {
|
||||
color := tui.ColInput
|
||||
if t.paused {
|
||||
color = tui.ColDisabled
|
||||
} else if t.trackBlocked {
|
||||
color = color.WithAttr(tui.Dim)
|
||||
}
|
||||
w.CPrint(color, string(before))
|
||||
w.CPrint(color, string(after))
|
||||
@@ -3031,6 +2917,18 @@ func (t *Terminal) printInfoImpl() {
|
||||
found := t.resultMerger.Length()
|
||||
total := max(found, t.count)
|
||||
output := fmt.Sprintf("%d/%d", found, total)
|
||||
if t.toggleSort {
|
||||
if t.sort {
|
||||
output += " +S"
|
||||
} else {
|
||||
output += " -S"
|
||||
}
|
||||
}
|
||||
if t.track.Global() {
|
||||
output += " +T"
|
||||
} else if t.track.Current() {
|
||||
output += " +t"
|
||||
}
|
||||
if t.multi > 0 {
|
||||
if t.multi == maxMulti {
|
||||
output += fmt.Sprintf(" (%d)", len(t.selected))
|
||||
@@ -3041,26 +2939,6 @@ func (t *Terminal) printInfoImpl() {
|
||||
if t.progress > 0 && t.progress < 100 {
|
||||
output += fmt.Sprintf(" (%d%%)", t.progress)
|
||||
}
|
||||
if t.toggleSort {
|
||||
if t.sort {
|
||||
output += " +S"
|
||||
} else {
|
||||
output += " -S"
|
||||
}
|
||||
}
|
||||
if t.track.Global() {
|
||||
if t.trackBlocked {
|
||||
output += " +T*"
|
||||
} else {
|
||||
output += " +T"
|
||||
}
|
||||
} else if t.track.Current() {
|
||||
if t.trackBlocked {
|
||||
output += " +t*"
|
||||
} else {
|
||||
output += " +t"
|
||||
}
|
||||
}
|
||||
if t.failed != nil && t.count == 0 {
|
||||
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
||||
}
|
||||
@@ -3261,7 +3139,7 @@ func (t *Terminal) printFooter() {
|
||||
func(markerClass) int {
|
||||
t.footerWindow.Print(indent)
|
||||
return indentSize
|
||||
}, nil, 0)
|
||||
}, nil)
|
||||
}
|
||||
})
|
||||
t.wrap = wrap
|
||||
@@ -3345,7 +3223,7 @@ func (t *Terminal) printHeaderImpl(window tui.Window, borderShape tui.BorderShap
|
||||
func(markerClass) int {
|
||||
t.window.Print(indent)
|
||||
return indentSize
|
||||
}, nil, 0)
|
||||
}, nil)
|
||||
}
|
||||
t.wrap = wrap
|
||||
}
|
||||
@@ -3583,14 +3461,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
||||
}
|
||||
return indentSize
|
||||
}
|
||||
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)
|
||||
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
|
||||
} else {
|
||||
preTask := func(marker markerClass) int {
|
||||
w := t.window.Width() - t.pointerLen
|
||||
@@ -3624,11 +3495,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
||||
base = base.WithBg(altBg)
|
||||
match = match.WithBg(altBg)
|
||||
}
|
||||
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)
|
||||
finalLineNum = t.printHighlighted(result, base, match, false, true, !matched, line, maxLine, forceRedraw, preTask, postTask)
|
||||
}
|
||||
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
|
||||
finalLineNum++
|
||||
@@ -3729,7 +3596,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
|
||||
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), nthOverlay tui.Attr) 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)) int {
|
||||
var displayWidth int
|
||||
item := result.item
|
||||
matchOffsets := []Offset{}
|
||||
@@ -3770,9 +3637,7 @@ 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
|
||||
// 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.
|
||||
fgAttr := tui.ColNormal.Attr()
|
||||
nthAttrFinal := fgAttr.Merge(t.nthAttr).Merge(nthOverlay)
|
||||
colBase = colBase.WithNewAttr(nthAttrFinal)
|
||||
colBase = colBase.WithAttr(t.nthAttr)
|
||||
}
|
||||
if !wholeCovered && t.nthAttr > 0 {
|
||||
var tokens []Token
|
||||
@@ -3791,7 +3656,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
sort.Sort(ByOrder(nthOffsets))
|
||||
}
|
||||
}
|
||||
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, nthOverlay, hidden)
|
||||
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, hidden)
|
||||
|
||||
// Determine split offset for horizontal scrolling with freeze
|
||||
splitOffset1 := -1
|
||||
@@ -3981,7 +3846,6 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
frozenRight = line[splitOffsetRight:]
|
||||
}
|
||||
displayWidthSum := 0
|
||||
displayWidthLeft := 0
|
||||
todo := [3]func(){}
|
||||
for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} {
|
||||
if len(runes) == 0 {
|
||||
@@ -4007,11 +3871,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
// For frozen parts, reserve space for the ellipsis in the middle part
|
||||
adjustedMaxWidth -= ellipsisWidth
|
||||
}
|
||||
var prefixWidth int
|
||||
if fidx == 2 {
|
||||
prefixWidth = displayWidthLeft
|
||||
}
|
||||
displayWidth = t.displayWidthWithLimit(runes, prefixWidth, adjustedMaxWidth)
|
||||
displayWidth = t.displayWidthWithLimit(runes, 0, adjustedMaxWidth)
|
||||
if !t.wrap && displayWidth > adjustedMaxWidth {
|
||||
maxe = util.Constrain(maxe+min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
|
||||
transformOffsets := func(diff int32) {
|
||||
@@ -4049,9 +3909,6 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
|
||||
}
|
||||
displayWidthSum += displayWidth
|
||||
if fidx == 0 {
|
||||
displayWidthLeft = displayWidth
|
||||
}
|
||||
|
||||
if maxWidth > 0 {
|
||||
color := colBase
|
||||
@@ -4059,7 +3916,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
color = color.WithFg(t.theme.Nomatch)
|
||||
}
|
||||
todo[fidx] = func() {
|
||||
t.printColoredString(t.window, runes, offs, color, prefixWidth)
|
||||
t.printColoredString(t.window, runes, offs, color)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
@@ -4086,13 +3943,10 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
return finalLineNum
|
||||
}
|
||||
|
||||
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair, initialPrefixWidth ...int) {
|
||||
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
|
||||
var index int32
|
||||
var substr string
|
||||
var prefixWidth int
|
||||
if len(initialPrefixWidth) > 0 {
|
||||
prefixWidth = initialPrefixWidth[0]
|
||||
}
|
||||
maxOffset := int32(len(text))
|
||||
var url *url
|
||||
for _, offset := range offsets {
|
||||
@@ -4299,7 +4153,7 @@ func (t *Terminal) followOffset() int {
|
||||
for i := len(body) - 1; i >= 0; i-- {
|
||||
h := t.previewLineHeight(body[i], maxWidth)
|
||||
if visualLines+h > height {
|
||||
return min(len(lines)-1, headerLines+i+1)
|
||||
return headerLines + i + 1
|
||||
}
|
||||
visualLines += h
|
||||
}
|
||||
@@ -4597,7 +4451,7 @@ Loop:
|
||||
}
|
||||
}
|
||||
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() || t.previewed.filled
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||
if fillRet == tui.FillNextLine {
|
||||
continue
|
||||
} else if fillRet == tui.FillSuspend {
|
||||
@@ -4620,7 +4474,7 @@ Loop:
|
||||
}
|
||||
lineNo++
|
||||
}
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.previewed.filled || index < len(lines)-1
|
||||
t.previewer.scrollable = t.previewer.scrollable || index < len(lines)-1
|
||||
t.previewed.image = image
|
||||
t.previewed.wireframe = wireframe
|
||||
}
|
||||
@@ -4630,7 +4484,7 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int)
|
||||
w := t.pborder.Width()
|
||||
xw := [2]int{t.pwindow.Left(), t.pwindow.Width()}
|
||||
redraw := false
|
||||
if len(t.previewer.bar) != height || t.previewer.xw != xw || t.previewed.version != t.previewer.version {
|
||||
if len(t.previewer.bar) != height || t.previewer.xw != xw {
|
||||
redraw = true
|
||||
t.previewer.bar = make([]bool, height)
|
||||
t.previewer.xw = xw
|
||||
@@ -5442,22 +5296,6 @@ func (t *Terminal) currentIndex() int32 {
|
||||
return minItem.Index()
|
||||
}
|
||||
|
||||
func (t *Terminal) trackKeyFor(item *Item, nth []Range) string {
|
||||
tokens := Tokenize(item.AsString(t.ansi), t.delimiter)
|
||||
return StripLastDelimiter(JoinTokens(Transform(tokens, nth)), t.delimiter)
|
||||
}
|
||||
|
||||
func (t *Terminal) unblockTrack() {
|
||||
if t.trackBlocked {
|
||||
t.trackBlocked = false
|
||||
t.trackKey = ""
|
||||
t.trackKeyCache = nil
|
||||
if !t.inputless {
|
||||
t.tui.ShowCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) addClickHeaderWord(env []string) []string {
|
||||
/*
|
||||
* echo $'HL1\nHL2' | fzf --header-lines 3 --header $'H1\nH2' --header-lines-border --bind 'click-header:preview:env | grep FZF_CLICK'
|
||||
@@ -6084,7 +5922,6 @@ func (t *Terminal) Loop() error {
|
||||
events := []util.EventType{}
|
||||
changed := false
|
||||
var newNth *[]Range
|
||||
var newWithNth *withNthSpec
|
||||
var newHeaderLines *int
|
||||
req := func(evts ...util.EventType) {
|
||||
for _, event := range evts {
|
||||
@@ -6102,7 +5939,6 @@ func (t *Terminal) Loop() error {
|
||||
events = []util.EventType{}
|
||||
changed = false
|
||||
newNth = nil
|
||||
newWithNth = nil
|
||||
newHeaderLines = nil
|
||||
beof := false
|
||||
queryChanged := false
|
||||
@@ -6279,14 +6115,6 @@ func (t *Terminal) Loop() error {
|
||||
callback(a.a)
|
||||
}
|
||||
}
|
||||
// When track-blocked, only allow abort/cancel and track-disabling actions
|
||||
if t.trackBlocked && a.t != actToggleTrack && a.t != actToggleTrackCurrent && a.t != actUntrackCurrent {
|
||||
if a.t == actAbort || a.t == actCancel {
|
||||
t.unblockTrack()
|
||||
req(reqPrompt, reqInfo)
|
||||
}
|
||||
return true
|
||||
}
|
||||
Action:
|
||||
switch a.t {
|
||||
case actIgnore, actStart, actClick:
|
||||
@@ -6502,33 +6330,6 @@ func (t *Terminal) Loop() error {
|
||||
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:
|
||||
t.input = []rune(a.a)
|
||||
t.cx = len(t.input)
|
||||
@@ -7058,16 +6859,14 @@ func (t *Terminal) Loop() error {
|
||||
case trackDisabled:
|
||||
t.track = trackEnabled
|
||||
}
|
||||
t.unblockTrack()
|
||||
req(reqPrompt, reqInfo)
|
||||
req(reqInfo)
|
||||
case actToggleTrackCurrent:
|
||||
if t.track.Current() {
|
||||
t.track = trackDisabled
|
||||
} else if t.track.Disabled() {
|
||||
t.track = trackCurrent(t.currentIndex())
|
||||
}
|
||||
t.unblockTrack()
|
||||
req(reqPrompt, reqInfo)
|
||||
req(reqInfo)
|
||||
case actShowHeader:
|
||||
t.headerVisible = true
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
@@ -7130,8 +6929,7 @@ func (t *Terminal) Loop() error {
|
||||
if t.track.Current() {
|
||||
t.track = trackDisabled
|
||||
}
|
||||
t.unblockTrack()
|
||||
req(reqPrompt, reqInfo)
|
||||
req(reqInfo)
|
||||
case actSearch:
|
||||
override := []rune(a.a)
|
||||
t.inputOverride = &override
|
||||
@@ -7184,12 +6982,10 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
if !me.Down {
|
||||
barDragging = false
|
||||
pmx, pmy = -1, -1
|
||||
}
|
||||
if !me.Down || !t.hasPreviewWindow() {
|
||||
pbarDragging = false
|
||||
pborderDragging = -1
|
||||
previewDraggingPos = -1
|
||||
pmx, pmy = -1, -1
|
||||
}
|
||||
|
||||
// Scrolling
|
||||
@@ -7217,7 +7013,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
|
||||
// Preview dragging
|
||||
if t.hasPreviewWindow() && me.Down && (previewDraggingPos >= 0 || click && t.pwindow.Enclose(my, mx)) {
|
||||
if me.Down && (previewDraggingPos >= 0 || click && t.hasPreviewWindow() && t.pwindow.Enclose(my, mx)) {
|
||||
if previewDraggingPos > 0 {
|
||||
scrollPreviewBy(previewDraggingPos - my)
|
||||
}
|
||||
@@ -7227,7 +7023,7 @@ func (t *Terminal) Loop() error {
|
||||
|
||||
// Preview scrollbar dragging
|
||||
headerLines := t.activePreviewOpts.headerLines
|
||||
pbarDragging = t.hasPreviewWindow() && me.Down && (pbarDragging || click && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
|
||||
pbarDragging = me.Down && (pbarDragging || click && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
|
||||
if pbarDragging {
|
||||
effectiveHeight := t.pwindow.Height() - headerLines
|
||||
numLines := len(t.previewer.lines) - headerLines
|
||||
@@ -7244,7 +7040,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
|
||||
// Preview border dragging (resizing)
|
||||
if t.hasPreviewWindow() && pborderDragging < 0 && click {
|
||||
if pborderDragging < 0 && click && t.hasPreviewWindow() {
|
||||
switch t.activePreviewOpts.position {
|
||||
case posUp:
|
||||
if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
|
||||
@@ -7273,7 +7069,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
}
|
||||
|
||||
if t.hasPreviewWindow() && pborderDragging >= 0 {
|
||||
if pborderDragging >= 0 && t.hasPreviewWindow() {
|
||||
var newSize int
|
||||
var prevSize int
|
||||
switch t.activePreviewOpts.position {
|
||||
@@ -7484,22 +7280,6 @@ func (t *Terminal) Loop() error {
|
||||
newCommand = &commandSpec{command, tempFiles}
|
||||
reloadSync = a.t == actReloadSync
|
||||
t.reading = true
|
||||
|
||||
if len(t.idNth) > 0 {
|
||||
t.trackSync = reloadSync
|
||||
}
|
||||
// Capture tracking key before reload
|
||||
if !t.track.Disabled() && len(t.idNth) > 0 {
|
||||
if item := t.currentItem(); item != nil {
|
||||
t.trackKey = t.trackKeyFor(item, t.idNth)
|
||||
t.trackKeyCache = make(map[int32]bool)
|
||||
t.trackBlocked = true
|
||||
if !t.inputless {
|
||||
t.tui.HideCursor()
|
||||
}
|
||||
req(reqPrompt, reqInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
case actUnbind:
|
||||
if keys, _, err := parseKeyChords(a.a, "PANIC"); err == nil {
|
||||
@@ -7697,17 +7477,12 @@ func (t *Terminal) Loop() error {
|
||||
reload := changed || newCommand != nil
|
||||
var reloadRequest *searchRequest
|
||||
if reload {
|
||||
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()}
|
||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, headerLines: newHeaderLines, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()}
|
||||
}
|
||||
|
||||
// Dispatch queued background requests
|
||||
t.dispatchAsync()
|
||||
|
||||
if t.pendingReqList {
|
||||
t.pendingReqList = false
|
||||
req(reqList)
|
||||
}
|
||||
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
if reload {
|
||||
@@ -7872,18 +7647,10 @@ func (t *Terminal) dumpItem(i *Item) StatusItem {
|
||||
if i == nil {
|
||||
return StatusItem{}
|
||||
}
|
||||
item := StatusItem{
|
||||
return StatusItem{
|
||||
Index: int(i.Index()),
|
||||
Text: i.AsString(t.ansi),
|
||||
}
|
||||
if t.resultMerger.pattern != nil {
|
||||
_, _, pos := t.resultMerger.pattern.MatchItem(i, true, t.slab)
|
||||
if pos != nil {
|
||||
sort.Ints(*pos)
|
||||
item.Positions = *pos
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (t *Terminal) tryLock(timeout time.Duration) bool {
|
||||
|
||||
30
src/tmux.go
30
src/tmux.go
@@ -1,11 +1,39 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func runTmux(args []string, opts *Options) (int, error) {
|
||||
argStr, dir := popupArgStr(args, opts)
|
||||
// Prepare arguments
|
||||
fzf, rest := args[0], args[1:]
|
||||
args = []string{"--bind=ctrl-z:ignore"}
|
||||
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
|
||||
// We append --border option at the end, because `--style=full:STYLE`
|
||||
// may have changed the default border style.
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range append(args, rest...) {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-tmux --no-height`
|
||||
|
||||
// Get current directory
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
|
||||
// Set tmux options for popup placement
|
||||
// C Both The centre of the terminal
|
||||
|
||||
@@ -161,7 +161,7 @@ func awkTokenizer(input string) ([]string, int) {
|
||||
end := 0
|
||||
for idx := 0; idx < len(input); idx++ {
|
||||
r := input[idx]
|
||||
white := r == 9 || r == 32 || r == 10
|
||||
white := r == 9 || r == 32
|
||||
switch state {
|
||||
case awkNil:
|
||||
if white {
|
||||
@@ -218,12 +218,11 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
// StripLastDelimiter removes the trailing delimiter
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
return strings.TrimSuffix(str, *delimiter.str)
|
||||
}
|
||||
if delimiter.regex != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
@@ -231,7 +230,6 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ func TestParseRange(t *testing.T) {
|
||||
|
||||
func TestTokenize(t *testing.T) {
|
||||
// AWK-style
|
||||
input := " abc: \n\t def: ghi "
|
||||
input := " abc: def: ghi "
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: \n\t " || tokens[0].prefixLength != 2 {
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ func TestTokenize(t *testing.T) {
|
||||
// With delimiter regex
|
||||
tokens = Tokenize(input, delimiterRegexp("\\s+"))
|
||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
|
||||
tokens[1].text.ToString() != "abc: \n\t " || tokens[1].prefixLength != 2 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 10 ||
|
||||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 16 {
|
||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
|
||||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,12 +447,6 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) WithNewAttr(attr Attr) ColorPair {
|
||||
dup := p
|
||||
dup.attr = attr
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
|
||||
dup := p
|
||||
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
|
||||
@@ -526,8 +520,6 @@ type ColorTheme struct {
|
||||
ListLabel ColorAttr
|
||||
ListBorder 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 {
|
||||
@@ -1207,19 +1199,13 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
||||
match.Attr = Underline
|
||||
}
|
||||
theme.Match = o(baseTheme.Match, match)
|
||||
// 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'
|
||||
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
|
||||
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
||||
current := theme.Current
|
||||
if !baseTheme.Colored && current.IsUndefined() {
|
||||
current.Attr |= Reverse
|
||||
}
|
||||
resolvedCurrent := o(baseTheme.Current, current)
|
||||
theme.NthCurrentAttr = resolvedCurrent.Attr
|
||||
theme.Current = theme.ListFg.Merge(resolvedCurrent)
|
||||
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
|
||||
currentMatch := theme.CurrentMatch
|
||||
if !baseTheme.Colored && currentMatch.IsUndefined() {
|
||||
currentMatch.Attr |= Reverse | Underline
|
||||
@@ -1244,8 +1230,10 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
|
||||
scrollbarDefined := theme.Scrollbar != undefined
|
||||
previewBorderDefined := theme.PreviewBorder != undefined
|
||||
|
||||
theme.NthSelectedAttr = theme.SelectedFg.Attr
|
||||
theme.SelectedFg = theme.ListFg.Merge(theme.SelectedFg)
|
||||
// These colors are not defined in the base themes
|
||||
theme.ListFg = o(theme.Fg, theme.ListFg)
|
||||
theme.ListBg = o(theme.Bg, theme.ListBg)
|
||||
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
||||
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -21,8 +20,8 @@ type Executor struct {
|
||||
|
||||
func NewExecutor(withShell string) *Executor {
|
||||
shell := os.Getenv("SHELL")
|
||||
args, err := shellwords.Parse(withShell)
|
||||
if err == nil && len(args) > 0 {
|
||||
args := strings.Fields(withShell)
|
||||
if len(args) > 0 {
|
||||
shell = args[0]
|
||||
args = args[1:]
|
||||
} else {
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type shellType int
|
||||
@@ -21,7 +19,6 @@ const (
|
||||
shellTypeUnknown shellType = iota
|
||||
shellTypeCmd
|
||||
shellTypePowerShell
|
||||
shellTypePwsh
|
||||
)
|
||||
|
||||
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
|
||||
@@ -49,10 +46,7 @@ func NewExecutor(withShell string) *Executor {
|
||||
} else if strings.HasPrefix(basename, "cmd") {
|
||||
shellType = shellTypeCmd
|
||||
args = []string{"/s/c"}
|
||||
} else if strings.HasPrefix(basename, "pwsh") {
|
||||
shellType = shellTypePwsh
|
||||
args = []string{"-NoProfile", "-Command"}
|
||||
} else if strings.HasPrefix(basename, "powershell") {
|
||||
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
|
||||
shellType = shellTypePowerShell
|
||||
args = []string{"-NoProfile", "-Command"}
|
||||
} else {
|
||||
@@ -62,12 +56,8 @@ func NewExecutor(withShell string) *Executor {
|
||||
}
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
//
|
||||
// On Windows, setpgid controls whether the spawned process is placed in a new
|
||||
// process group (so that it can be signaled independently, e.g. for previews).
|
||||
// However, we only do this for "pwsh" and non-standard shells, because cmd.exe
|
||||
// and Windows PowerShell ("powershell.exe") don't always exit on Ctrl-Break.
|
||||
//
|
||||
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||
// can kill preview process with its child processes at once.
|
||||
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
||||
// but it is left as is now because no adverse effect has been observed.
|
||||
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||
@@ -83,31 +73,19 @@ func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||
}
|
||||
x.shellPath.Store(shell)
|
||||
}
|
||||
|
||||
var creationFlags uint32
|
||||
// Set new process group for pwsh (PowerShell 7+) and unknown/posix-ish shells
|
||||
if setpgid && (x.shellType == shellTypePwsh || x.shellType == shellTypeUnknown) {
|
||||
creationFlags = windows.CREATE_NEW_PROCESS_GROUP
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if x.shellType == shellTypeCmd {
|
||||
cmd = exec.Command(shell)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: false,
|
||||
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
|
||||
CreationFlags: creationFlags,
|
||||
CreationFlags: 0,
|
||||
}
|
||||
} else {
|
||||
args := x.args
|
||||
if setpgid && x.shellType == shellTypePwsh {
|
||||
// pwsh needs -NonInteractive flag to exit on Ctrl-Break
|
||||
args = append([]string{"-NonInteractive"}, x.args...)
|
||||
}
|
||||
cmd = exec.Command(shell, append(args, command)...)
|
||||
cmd = exec.Command(shell, append(x.args, command)...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: false,
|
||||
CreationFlags: creationFlags,
|
||||
CreationFlags: 0,
|
||||
}
|
||||
}
|
||||
return cmd
|
||||
@@ -178,7 +156,7 @@ func (x *Executor) QuoteEntry(entry string) string {
|
||||
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
|
||||
*/
|
||||
return escapeArg(entry)
|
||||
case shellTypePowerShell, shellTypePwsh:
|
||||
case shellTypePowerShell:
|
||||
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
||||
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
||||
default:
|
||||
@@ -188,21 +166,6 @@ func (x *Executor) QuoteEntry(entry string) string {
|
||||
|
||||
// KillCommand kills the process for the given command
|
||||
func KillCommand(cmd *exec.Cmd) error {
|
||||
// Safely handle nil command or process.
|
||||
if cmd == nil || cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
// If it has its own process group, we can send it Ctrl-Break
|
||||
if cmd.SysProcAttr != nil && cmd.SysProcAttr.CreationFlags&windows.CREATE_NEW_PROCESS_GROUP != 0 {
|
||||
if err := windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(cmd.Process.Pid)); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// If it's the same process group, or if sending the console control event
|
||||
// fails (e.g., no console, different console, or process already exited),
|
||||
// fall back to a standard kill. This probably won't *help* if there's I/O
|
||||
// going on, because Wait() will still hang until the I/O finishes unless we
|
||||
// hard-kill the entire process group. But it doesn't hurt to try!
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func runZellij(args []string, opts *Options) (int, error) {
|
||||
argStr, dir := popupArgStr(args, opts)
|
||||
|
||||
zellijArgs := []string{
|
||||
"run", "--floating", "--close-on-exit", "--block-until-exit",
|
||||
"--cwd", dir,
|
||||
}
|
||||
if !opts.Tmux.border {
|
||||
zellijArgs = append(zellijArgs, "--borderless", "true")
|
||||
}
|
||||
switch opts.Tmux.position {
|
||||
case posUp:
|
||||
zellijArgs = append(zellijArgs, "-y", "0")
|
||||
case posDown:
|
||||
zellijArgs = append(zellijArgs, "-y", "9999")
|
||||
case posLeft:
|
||||
zellijArgs = append(zellijArgs, "-x", "0")
|
||||
case posRight:
|
||||
zellijArgs = append(zellijArgs, "-x", "9999")
|
||||
case posCenter:
|
||||
// Zellij centers floating panes by default
|
||||
}
|
||||
zellijArgs = append(zellijArgs, "--width", opts.Tmux.width.String())
|
||||
zellijArgs = append(zellijArgs, "--height", opts.Tmux.height.String())
|
||||
zellijArgs = append(zellijArgs, "--")
|
||||
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zellijArgs = append(zellijArgs, sh, temp)
|
||||
return exec.Command("zellij", zellijArgs...), nil
|
||||
}, opts, true)
|
||||
}
|
||||
@@ -3,9 +3,10 @@ set -e FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE FZF_TMUX FZF_T
|
||||
set -e FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
set -e FZF_API_KEY
|
||||
# Unset completion-specific variables
|
||||
set -e FZF_COMPLETION_OPTS FZF_EXPANSION_OPTS
|
||||
set -e FZF_COMPLETION_TRIGGER FZF_COMPLETION_OPTS
|
||||
|
||||
set -gx FZF_DEFAULT_OPTS "--no-scrollbar --pointer '>' --marker '>'"
|
||||
set -gx FZF_COMPLETION_TRIGGER '++'
|
||||
set -gx fish_history fzf_test
|
||||
|
||||
# Add fzf to PATH
|
||||
|
||||
@@ -404,11 +404,11 @@ class TestCore < TestInteractive
|
||||
tmux.send_keys "seq 1 111 | #{fzf("-m +s --tac #{opt} -q11")}", :Enter
|
||||
tmux.until { |lines| assert_equal '> 111', lines[-3] }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal ' 4/111 (1) -S', lines[-2] }
|
||||
tmux.until { |lines| assert_equal ' 4/111 -S (1)', lines[-2] }
|
||||
tmux.send_keys 'C-R'
|
||||
tmux.until { |lines| assert_equal '> 11', lines[-3] }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal ' 4/111 (2) +S', lines[-2] }
|
||||
tmux.until { |lines| assert_equal ' 4/111 +S (2)', lines[-2] }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[111 11], fzf_output_lines
|
||||
end
|
||||
@@ -1190,16 +1190,6 @@ class TestCore < TestInteractive
|
||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||
end
|
||||
|
||||
def test_freeze_left_tabstop
|
||||
writelines(%W[1\t2\t3])
|
||||
# With --freeze-left 1 and --tabstop=2:
|
||||
# Frozen left: "1" (width 1)
|
||||
# Middle starts with "\t" at prefix width 1, tabstop 2 → 1 space
|
||||
# Then "2" at column 2, next "\t" at column 3 → 1 space, then "3"
|
||||
tmux.send_keys %(cat #{tempname} | #{FZF} --tabstop=2 --freeze-left 1), :Enter
|
||||
tmux.until { |lines| assert_equal '> 1 2 3', lines[-3] }
|
||||
end
|
||||
|
||||
def test_freeze_left_keep_right
|
||||
tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter
|
||||
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
|
||||
@@ -1659,236 +1649,6 @@ class TestCore < TestInteractive
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_whole_line
|
||||
# --track --id-nth .. should track by entire line across reloads
|
||||
tmux.send_keys "seq 1000 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:seq 1000 | sort -R'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
|
||||
# Move to item 555
|
||||
tmux.send_keys '555'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||
|
||||
# Reload with shuffled order — cursor should track "555"
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
assert_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_field
|
||||
# --track --id-nth 1 should track by first field across reloads
|
||||
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --track --id-nth 1 --bind 'ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines, '> 1 apple'
|
||||
end
|
||||
|
||||
# Move up to "2 banana"
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
||||
|
||||
# Reload — the second field changes, but first field "2" stays
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines, '> 2 blueberry'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_no_match
|
||||
# When tracked item is not found after reload, cursor stays at current position
|
||||
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||
|
||||
# Reload with completely different items — no match for "beta"
|
||||
# Cursor stays at the same position (second item)
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines, '> epsilon'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_blocked_indicator
|
||||
# +T* should appear during reload and disappear when match is found
|
||||
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; seq 100 | sort -R'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
|
||||
# Trigger slow reload — should show +T* while blocked
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||
|
||||
# After reload completes, +T* should clear back to +T
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_abort_unblocks
|
||||
# Escape during track-blocked state should unblock, not quit
|
||||
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 3; seq 100'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
|
||||
# Trigger slow reload
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||
|
||||
# Escape should unblock, not quit fzf
|
||||
tmux.send_keys :Escape
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_async_unblocks_early
|
||||
# With async reload, +T* should clear as soon as the match streams in,
|
||||
# even while loading is still in progress.
|
||||
# sleep 1 first so +T* is observable, then the match arrives, then more items after a delay.
|
||||
tmux.send_keys "seq 5 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 5, lines.match_count
|
||||
assert_includes lines, '> 1'
|
||||
end
|
||||
|
||||
# Trigger reload — blocked during initial sleep
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||
# Match "1" arrives, unblocks before the remaining items load
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 1'
|
||||
assert_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_sync_blocks_until_complete
|
||||
# With reload-sync, +T* should stay until the entire stream is complete,
|
||||
# even though the match arrives early in the stream.
|
||||
tmux.send_keys "seq 5 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload-sync:sleep 1; echo 1; sleep 2; seq 2 10'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 5, lines.match_count
|
||||
assert_includes lines, '> 1'
|
||||
end
|
||||
|
||||
# Trigger reload-sync — every observable state must be either:
|
||||
# 1. +T* (still blocked), or
|
||||
# 2. final state (count=10, +T without *)
|
||||
# Any other combination (e.g. unblocked while count < 10) is a bug.
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
info = lines[-2]
|
||||
blocked = info&.include?('+T*')
|
||||
unless blocked
|
||||
raise "Unblocked before stream complete (count: #{lines.match_count})" if lines.match_count != 10
|
||||
|
||||
assert_includes info, '+T'
|
||||
assert_includes lines, '> 1'
|
||||
end
|
||||
!blocked
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_toggle_track_unblocks
|
||||
# toggle-track during track-blocked state should unblock and disable tracking
|
||||
tmux.send_keys "seq 100 | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 5; seq 100' --bind 'ctrl-t:toggle-track'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
|
||||
# Trigger slow reload
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||
|
||||
# toggle-track should unblock and disable tracking before reload completes
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until(timeout: 3) do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_nth_reload_async_no_match
|
||||
# With async reload, when tracked item is not found, cursor stays at
|
||||
# current position after stream completes
|
||||
tmux.send_keys "printf 'alpha\\nbeta\\ngamma\\n' | #{FZF} --track --id-nth .. --bind 'ctrl-r:reload:sleep 1; printf \"delta\\nepsilon\\nzeta\\n\"'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> beta' }
|
||||
|
||||
# Reload with completely different items — no match for "beta"
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+T*' }
|
||||
# After stream completes, unblocks with cursor at same position (second item)
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines, '> epsilon'
|
||||
refute_includes lines[-2], '+T*'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_action_with_id_nth
|
||||
# track-current with --id-nth should track by specified field
|
||||
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{FZF} --id-nth 1 --bind 'ctrl-t:track-current,ctrl-r:reload:printf \"1 apricot\\n2 blueberry\\n3 cranberry\\n\"'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
|
||||
# Move to "2 banana" and activate tracking
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2 banana' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_includes lines[-2], '+t' }
|
||||
|
||||
# Reload — should track by field "2"
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines, '> 2 blueberry'
|
||||
end
|
||||
end
|
||||
|
||||
def test_id_nth_preserve_multi_selection
|
||||
# --id-nth with --multi should preserve selections across reload-sync
|
||||
File.write(tempname, "1 apricot\n2 blueberry\n3 cranberry\n")
|
||||
tmux.send_keys "printf '1 apple\\n2 banana\\n3 cherry\\n' | #{fzf("--multi --id-nth 1 --bind 'ctrl-r:reload-sync:cat #{tempname}'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
|
||||
# Select first item (1 apple) and third item (3 cherry)
|
||||
tmux.send_keys :Tab
|
||||
tmux.send_keys :Up, :Up, :Tab
|
||||
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||
|
||||
# Reload — selections should be preserved by id-nth key
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.match_count
|
||||
assert_includes lines[-2], '(2)'
|
||||
assert(lines.any? { |l| l.include?('apricot') })
|
||||
end
|
||||
|
||||
# Accept and verify the correct items were preserved
|
||||
tmux.send_keys :Enter
|
||||
assert_equal ['1 apricot', '3 cranberry'], fzf_output_lines
|
||||
end
|
||||
|
||||
def test_one_and_zero
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||
tmux.send_keys '1'
|
||||
@@ -1985,191 +1745,6 @@ class TestCore < TestInteractive
|
||||
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 env_vars
|
||||
return {} unless File.exist?(tempname)
|
||||
@@ -2325,13 +1900,13 @@ class TestCore < TestInteractive
|
||||
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter is removed
|
||||
assert_equal ['bar,bar,foo ,bazfoo '], File.readlines(tempname, chomp: true)
|
||||
# Last delimiter and the whitespaces are removed
|
||||
assert_equal ['bar,bar,foo ,bazfoo'], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_accept_nth_regex_delimiter
|
||||
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter=' *[:,]+ *' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
|
||||
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter='[:,]+' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter and the whitespaces are removed
|
||||
@@ -2349,7 +1924,7 @@ class TestCore < TestInteractive
|
||||
end
|
||||
|
||||
def test_accept_nth_template
|
||||
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d " *, *" --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
|
||||
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
# Last delimiter and the whitespaces are removed
|
||||
|
||||
@@ -393,20 +393,6 @@ class TestPreview < TestInteractive
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_follow_wrap_long_line
|
||||
tmux.send_keys %(seq 1 | #{FZF} --preview "seq 2; yes yes | head -10000 | tr '\n' ' '" --preview-window follow,wrap --bind up:preview-up,down:preview-down), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?('3/3 │')
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert lines.any_include?('2/3 │') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert lines.any_include?('1/3 │') }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert lines.any_include?('2/3 │') }
|
||||
end
|
||||
|
||||
def test_close
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
@@ -607,7 +593,7 @@ class TestPreview < TestInteractive
|
||||
end
|
||||
|
||||
def test_preview_wrap_sign_between_ansi_fragments_overflow
|
||||
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m123 \\x1b[mhi"; echo -e "\\x1b[33m123 \\x1b[mhi"' --preview-window 2,wrap-word,noinfo), :Enter
|
||||
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 2,wrap-word), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(2, lines.count { |line| line.include?('│ 12 │') })
|
||||
@@ -616,27 +602,11 @@ class TestPreview < TestInteractive
|
||||
end
|
||||
|
||||
def test_preview_wrap_sign_between_ansi_fragments_overflow2
|
||||
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m123 \\x1b[mhi"; echo -e "\\x1b[33m123 \\x1b[mhi"' --preview-window 1,wrap-word,noinfo), :Enter
|
||||
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 1,wrap-word), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(2, lines.count { |line| line.include?('│ 1 │') })
|
||||
assert_equal(0, lines.count { |line| line.include?('│ h') })
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_toggle_should_redraw_scrollbar
|
||||
tmux.send_keys %(seq 1 | #{FZF} --no-border --scrollbar --preview 'seq $((FZF_PREVIEW_LINES + 1))' --preview-border line --bind tab:toggle-preview --header foo --header-border --footer bar --footer-border), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_operator lines.count { |line| line.end_with?('│') }, :>, 2
|
||||
end
|
||||
tmux.send_keys :Tab
|
||||
tmux.until do |lines|
|
||||
assert_equal(2, lines.count { |line| line.end_with?('│') })
|
||||
end
|
||||
tmux.send_keys :Tab
|
||||
tmux.until do |lines|
|
||||
assert_operator lines.count { |line| line.end_with?('│') }, :>, 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,31 +16,6 @@ class TestServer < TestInteractive
|
||||
assert_empty state[:query]
|
||||
assert_equal({ index: 0, text: '1' }, state[:current])
|
||||
|
||||
# No positions when query is empty
|
||||
state[:matches].each do |m|
|
||||
assert_nil m[:positions]
|
||||
end
|
||||
assert_nil state[:current][:positions] if state[:current]
|
||||
|
||||
# Positions with a single-character query
|
||||
Net::HTTP.post(fn.call, 'change-query(1)')
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal [0], state[:current][:positions]
|
||||
state[:matches].each do |m|
|
||||
assert_includes m[:text], '1'
|
||||
assert_equal [m[:text].index('1')], m[:positions]
|
||||
end
|
||||
|
||||
# Positions with a multi-character query; verify sorted ascending
|
||||
Net::HTTP.post(fn.call, 'change-query(10)')
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal '10', state[:current][:text]
|
||||
assert_equal [0, 1], state[:current][:positions]
|
||||
assert_equal state[:current][:positions], state[:current][:positions].sort
|
||||
|
||||
# No match — no current item
|
||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
|
||||
@@ -832,55 +832,6 @@ class TestBash < TestBase
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_r_delete
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo to-keep', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo to-delete-1', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo to-delete-2', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo to-delete-3', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo another-keeper', :Enter
|
||||
tmux.prepare
|
||||
|
||||
# Open Ctrl-R and delete one entry
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'to-delete'
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys 'S-Delete'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
# Multi-select remaining two and delete them at once
|
||||
tmux.send_keys :BTab, :BTab
|
||||
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||
tmux.send_keys 'S-Delete'
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
|
||||
# Exit without selecting
|
||||
tmux.send_keys :Escape
|
||||
tmux.prepare
|
||||
|
||||
# Verify deleted entries are gone from history
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'to-delete'
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Escape
|
||||
tmux.prepare
|
||||
|
||||
# Verify kept entries are still there
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'to-keep'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo to-keep', lines[-1] }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
|
||||
def test_dynamic_completion_loader
|
||||
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
||||
tmux.paste '_completion_loader() { complete -o default fake; }'
|
||||
@@ -969,7 +920,6 @@ class TestZsh < TestBase
|
||||
end
|
||||
|
||||
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
|
||||
tmux.send_keys 'setopt sh_glob', :Enter
|
||||
# Leading number in multi-line history content is not confused with index
|
||||
prepare_ctrl_r_test
|
||||
tmux.send_keys "'line 1"
|
||||
|
||||
@@ -5,7 +5,6 @@ fo = "fo"
|
||||
enew = "enew"
|
||||
tabe = "tabe"
|
||||
Iterm = "Iterm"
|
||||
ser = "ser"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["README.md", "*.s"]
|
||||
extend-exclude = ["README.md"]
|
||||
|
||||
Reference in New Issue
Block a user