mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 23:03:47 -05:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0d407f7ce | ||
|
|
461115afde | ||
|
|
bae1965231 | ||
|
|
b89c77ec9a | ||
|
|
1ca5f09d7b | ||
|
|
d79902ae59 | ||
|
|
77568e114f | ||
|
|
a24d274a3c | ||
|
|
dac81432d6 | ||
|
|
309b5081ef | ||
|
|
91bc4f2671 | ||
|
|
4c9d37d919 | ||
|
|
7e9566f66a | ||
|
|
3f7e8a475d | ||
|
|
1cf7c0f334 | ||
|
|
ff8ee9ee4e | ||
|
|
cbbd939a94 | ||
|
|
f232df2887 | ||
|
|
16bfb2c80c | ||
|
|
0ba066123e | ||
|
|
81c51c26cc | ||
|
|
6fa8295ac5 | ||
|
|
f975b40236 | ||
|
|
01d9d9c8c8 | ||
|
|
1eafc4e5d9 | ||
|
|
38e4020aa8 | ||
|
|
ac32fbb3b2 | ||
|
|
7d26eca5cc | ||
|
|
3347d61591 | ||
|
|
9abf2c8c9c | ||
|
|
84e2262ad6 | ||
|
|
378137d34a | ||
|
|
66ca16f836 | ||
|
|
282884ad83 | ||
|
|
7877ac42f0 | ||
|
|
19ef8891e3 | ||
|
|
bfea9e53a6 | ||
|
|
a2420026ab | ||
|
|
1be1991299 | ||
|
|
67dd7e1923 | ||
|
|
2b584586ed | ||
|
|
a1994ff0ab | ||
|
|
ca0e858871 | ||
|
|
06c6615507 | ||
|
|
818d0be436 | ||
|
|
fcd2baa945 | ||
|
|
62e0a2824a | ||
|
|
bbe1721a18 | ||
|
|
c1470a51b8 | ||
|
|
6ee31d5dc5 | ||
|
|
65d74387e7 | ||
|
|
7d0ea599c4 | ||
|
|
b7795a3dea | ||
|
|
323f6f6202 | ||
|
|
0c61223884 | ||
|
|
32234be7a2 | ||
|
|
178b49832e | ||
|
|
18cbb4a84d | ||
|
|
e84afe196a | ||
|
|
e1e171a3c4 | ||
|
|
d075c00015 | ||
|
|
6c0ca4a64a | ||
|
|
6b5d461411 | ||
|
|
7419e0dde1 | ||
|
|
cf2bb5e40e | ||
|
|
f466e94d65 | ||
|
|
eb0257d48f | ||
|
|
b83dd6c6b4 | ||
|
|
51c207448d | ||
|
|
a6a558da30 | ||
|
|
2bf5fa27be | ||
|
|
af7940746f | ||
|
|
a2aa1a156c | ||
|
|
2f8a72a42a | ||
|
|
8179ca5eaa | ||
|
|
4b74f882c7 | ||
|
|
7cf45af502 | ||
|
|
46c21158d8 | ||
|
|
80da0776f8 | ||
|
|
e91f10ab16 | ||
|
|
2c15cd7923 | ||
|
|
d6584543e9 | ||
|
|
c13228f346 | ||
|
|
7220d8233e | ||
|
|
0237bf09bf | ||
|
|
04017c25bb | ||
|
|
02199cd609 | ||
|
|
26b9f5831a | ||
|
|
243a76002c | ||
|
|
c71e4ddee4 | ||
|
|
32eb8c1be9 | ||
|
|
c587017830 | ||
|
|
fb885652cc | ||
|
|
afc2f05e5e | ||
|
|
06547d0cbe | ||
|
|
578108280e |
10
.github/workflows/linux.yml
vendored
10
.github/workflows/linux.yml
vendored
@@ -16,7 +16,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -30,19 +30,19 @@ jobs:
|
|||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.1.0
|
ruby-version: 3.4.1
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: sudo apt-get install --yes zsh fish tmux
|
run: sudo apt-get install --yes zsh fish tmux
|
||||||
|
|
||||||
- name: Install Ruby gems
|
- name: Install Ruby gems
|
||||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
run: bundle install
|
||||||
|
|
||||||
- name: Rubocop
|
- name: Rubocop
|
||||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
run: make lint
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
- name: Integration test
|
- name: Integration test
|
||||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||||
|
|||||||
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: crate-ci/typos@v1.28.4
|
- uses: crate-ci/typos@v1.29.4
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
|||||||
dist
|
dist
|
||||||
target
|
target
|
||||||
pkg
|
pkg
|
||||||
Gemfile.lock
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
doc/tags
|
doc/tags
|
||||||
vendor
|
vendor
|
||||||
|
|||||||
10
.rubocop.yml
10
.rubocop.yml
@@ -1,9 +1,13 @@
|
|||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics:
|
Metrics:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Lint/ShadowingOuterLocalVariable:
|
Lint/ShadowingOuterLocalVariable:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Lint/NestedMethodDefinition:
|
||||||
|
Enabled: false
|
||||||
Style/MethodCallWithArgsParentheses:
|
Style/MethodCallWithArgsParentheses:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
AllowedMethods:
|
AllowedMethods:
|
||||||
@@ -28,5 +32,11 @@ Style/WordArray:
|
|||||||
MinSize: 1
|
MinSize: 1
|
||||||
Minitest/AssertEqual:
|
Minitest/AssertEqual:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Minitest/EmptyLineBeforeAssertionMethods:
|
||||||
|
Enabled: false
|
||||||
Naming/VariableNumber:
|
Naming/VariableNumber:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Lint/EmptyBlock:
|
||||||
|
Enabled: false
|
||||||
|
Style/SafeNavigationChainLength:
|
||||||
|
Enabled: false
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
golang 1.20.13
|
golang 1.20.13
|
||||||
|
ruby 3.4.1
|
||||||
|
|||||||
48
ADVANCED.md
48
ADVANCED.md
@@ -1,8 +1,8 @@
|
|||||||
Advanced fzf examples
|
Advanced fzf examples
|
||||||
======================
|
======================
|
||||||
|
|
||||||
* *Last update: 2024/06/24*
|
* *Last update: 2025/02/02*
|
||||||
* *Requires fzf 0.54.0 or later*
|
* *Requires fzf 0.59.0 or later*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ Advanced fzf examples
|
|||||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||||
|
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||||
* [Log tailing](#log-tailing)
|
* [Log tailing](#log-tailing)
|
||||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||||
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
*(See `Layout` section of the man page to see the full list of options)*
|
*(See man page to see the full list of options)*
|
||||||
|
|
||||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||||
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
|||||||
--bind 'enter:become(vim {1} +{2})'
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Controlling Ripgrep search and fzf search simultaneously
|
||||||
|
|
||||||
|
`search` and `transform-search` action allow you to trigger an fzf search with
|
||||||
|
an arbitrary query string. This frees fzf from strictly following the prompt
|
||||||
|
input, enabling custom search syntax.
|
||||||
|
|
||||||
|
In the example below, `transform` action is used to conditionally trigger
|
||||||
|
`reload` for ripgrep, followed by `search` for fzf. The first word of the
|
||||||
|
query initiates the Ripgrep process to generate the initial results, while the
|
||||||
|
remainder of the query is passed to fzf for secondary filtering.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export TEMP=$(mktemp -u)
|
||||||
|
trap 'rm -f "$TEMP"' EXIT
|
||||||
|
|
||||||
|
INITIAL_QUERY="${*:-}"
|
||||||
|
TRANSFORMER='
|
||||||
|
rg_pat={q:1} # The first word is passed to ripgrep
|
||||||
|
fzf_pat={q:2..} # The rest are passed to fzf
|
||||||
|
|
||||||
|
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||||
|
echo "$rg_pat" > "$TEMP"
|
||||||
|
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||||
|
fi
|
||||||
|
echo "+search:$fzf_pat"
|
||||||
|
'
|
||||||
|
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
|
--with-shell 'bash -c' \
|
||||||
|
--bind "start,change:transform:$TRANSFORMER" \
|
||||||
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
|
--delimiter : \
|
||||||
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
|
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
|
||||||
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
|
```
|
||||||
|
|
||||||
Log tailing
|
Log tailing
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@@ -529,8 +568,7 @@ pods() {
|
|||||||
--info=inline --layout=reverse --header-lines=1 \
|
--info=inline --layout=reverse --header-lines=1 \
|
||||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||||
--bind 'start:reload:$command' \
|
--bind 'start,ctrl-r:reload:$command' \
|
||||||
--bind 'ctrl-r:reload:$command' \
|
|
||||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||||
|
|||||||
145
CHANGELOG.md
145
CHANGELOG.md
@@ -1,6 +1,151 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.60.2
|
||||||
|
------
|
||||||
|
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
|
||||||
|
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
|
||||||
|
- Thanks to @phanen for the fix
|
||||||
|
- Fixed 'jump' action when the pointer is an empty string
|
||||||
|
|
||||||
|
0.60.1
|
||||||
|
------
|
||||||
|
- Bug fixes and minor improvements
|
||||||
|
- Built-in walker now prints directory entries with a trailing slash
|
||||||
|
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
|
||||||
|
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
|
||||||
|
|
||||||
|
0.60.0
|
||||||
|
------
|
||||||
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
|
||||||
|
|
||||||
|
- Added `--accept-nth` for choosing output fields
|
||||||
|
```sh
|
||||||
|
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
|
||||||
|
# Becomes
|
||||||
|
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
|
||||||
|
|
||||||
|
git branch | fzf | cut -c3-
|
||||||
|
# Can be rewritten as
|
||||||
|
git branch | fzf --accept-nth -1
|
||||||
|
```
|
||||||
|
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
|
||||||
|
```sh
|
||||||
|
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
|
||||||
|
# foo, baz, bar
|
||||||
|
|
||||||
|
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||||
|
# foo,baz,bar,foo,bar
|
||||||
|
```
|
||||||
|
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
|
||||||
|
```sh
|
||||||
|
seq 100 | fzf --bind 'ctrl-x:exclude'
|
||||||
|
|
||||||
|
# 'exclude-multi' will exclude the selected items or the current item
|
||||||
|
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
|
||||||
|
```
|
||||||
|
- Preview window now prints wrap indicator when wrapping is enabled
|
||||||
|
```sh
|
||||||
|
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
|
||||||
|
```
|
||||||
|
- Bug fixes and improvements
|
||||||
|
|
||||||
|
0.59.0
|
||||||
|
------
|
||||||
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
|
||||||
|
|
||||||
|
- Prioritizing file name matches (#4192)
|
||||||
|
- Added a new tiebreak option `pathname` for prioritizing file name matches
|
||||||
|
- `--scheme=path` now sets `--tiebreak=pathname,length`
|
||||||
|
- fzf will automatically choose `path` scheme
|
||||||
|
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
|
||||||
|
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
|
||||||
|
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
|
||||||
|
```sh
|
||||||
|
# Use --header-lines-border to separate two headers
|
||||||
|
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||||
|
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
||||||
|
--header-lines-border bottom --no-list-border
|
||||||
|
```
|
||||||
|
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
|
||||||
|
```sh
|
||||||
|
# Click on the header line to limit search scope
|
||||||
|
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||||
|
--header-lines-border bottom --no-list-border \
|
||||||
|
--color fg:dim,nth:regular \
|
||||||
|
--bind 'click-header:transform-nth(
|
||||||
|
echo $FZF_CLICK_HEADER_NTH
|
||||||
|
)+transform-prompt(
|
||||||
|
echo "$FZF_CLICK_HEADER_WORD> "
|
||||||
|
)'
|
||||||
|
```
|
||||||
|
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
|
||||||
|
- `kill` completion for bash and zsh were updated to use this feature
|
||||||
|
- Added `--no-input` option to completely disable and hide the input section
|
||||||
|
```sh
|
||||||
|
# Click header to trigger search
|
||||||
|
fzf --header '[src] [test]' --no-input --layout reverse \
|
||||||
|
--header-border bottom --input-border \
|
||||||
|
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
|
||||||
|
|
||||||
|
# Vim-like mode switch
|
||||||
|
fzf --layout reverse-list --no-input \
|
||||||
|
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
|
||||||
|
--bind 'enter,esc,ctrl-c:transform:
|
||||||
|
if [[ $FZF_INPUT_STATE = enabled ]]; then
|
||||||
|
echo "rebind(j,k,/)+hide-input"
|
||||||
|
elif [[ $FZF_KEY = enter ]]; then
|
||||||
|
echo accept
|
||||||
|
else
|
||||||
|
echo abort
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
```
|
||||||
|
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
|
||||||
|
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
|
||||||
|
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
|
||||||
|
```sh
|
||||||
|
export TEMP=$(mktemp -u)
|
||||||
|
trap 'rm -f "$TEMP"' EXIT
|
||||||
|
|
||||||
|
TRANSFORMER='
|
||||||
|
rg_pat={q:1} # The first word is passed to ripgrep
|
||||||
|
fzf_pat={q:2..} # The rest are passed to fzf
|
||||||
|
|
||||||
|
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||||
|
echo "$rg_pat" > "$TEMP"
|
||||||
|
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||||
|
fi
|
||||||
|
echo "+search:$fzf_pat"
|
||||||
|
'
|
||||||
|
fzf --ansi --disabled \
|
||||||
|
--with-shell 'bash -c' \
|
||||||
|
--bind "start,change:transform:$TRANSFORMER"
|
||||||
|
```
|
||||||
|
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
|
||||||
|
```sh
|
||||||
|
# Load 'ps -ef' output on start and reload it on CTRL-R
|
||||||
|
fzf --bind 'start,ctrl-r:reload:ps -ef'
|
||||||
|
```
|
||||||
|
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
|
||||||
|
```sh
|
||||||
|
# You will only see the input section which takes 3 lines
|
||||||
|
fzf --style=full --height 1% --min-height 3
|
||||||
|
|
||||||
|
# You will see 3 items in the list section
|
||||||
|
fzf --style full --height 1% --min-height 3+
|
||||||
|
```
|
||||||
|
- Shell integration scripts were updated to use `--min-height 20+` by default
|
||||||
|
- `--header-lines` will be displayed at the top in `reverse-list` layout
|
||||||
|
- Added `bell` action to ring the terminal bell
|
||||||
|
```sh
|
||||||
|
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
|
||||||
|
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
|
||||||
|
```
|
||||||
|
- Added `toggle-bind` action
|
||||||
|
- Bug fixes and improvements
|
||||||
|
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
|
||||||
|
|
||||||
0.58.0
|
0.58.0
|
||||||
------
|
------
|
||||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM ubuntu:24.04
|
FROM rubylang/ruby:3.4.1-noble
|
||||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||||
RUN gem install --no-document -v 5.22.3 minitest
|
RUN gem install --no-document -v 5.22.3 minitest
|
||||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||||
@@ -9,4 +9,4 @@ RUN rm -f /etc/bash.bashrc
|
|||||||
COPY . /fzf
|
COPY . /fzf
|
||||||
RUN cd /fzf && make install && ./install --all
|
RUN cd /fzf && make install && ./install --all
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||||
|
|||||||
8
Gemfile
Normal file
8
Gemfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'minitest', '5.25.4'
|
||||||
|
gem 'rubocop', '1.71.0'
|
||||||
|
gem 'rubocop-minitest', '0.36.0'
|
||||||
|
gem 'rubocop-performance', '1.23.1'
|
||||||
47
Gemfile.lock
Normal file
47
Gemfile.lock
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
ast (2.4.2)
|
||||||
|
json (2.9.1)
|
||||||
|
language_server-protocol (3.17.0.3)
|
||||||
|
minitest (5.25.4)
|
||||||
|
parallel (1.26.3)
|
||||||
|
parser (3.3.7.0)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
|
racc (1.8.1)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
regexp_parser (2.10.0)
|
||||||
|
rubocop (1.71.0)
|
||||||
|
json (~> 2.3)
|
||||||
|
language_server-protocol (>= 3.17.0)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.3.0.2)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
|
rubocop-ast (>= 1.36.2, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
|
rubocop-ast (1.37.0)
|
||||||
|
parser (>= 3.3.1.0)
|
||||||
|
rubocop-minitest (0.36.0)
|
||||||
|
rubocop (>= 1.61, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-performance (1.23.1)
|
||||||
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
arm64-darwin-23
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
minitest (= 5.25.4)
|
||||||
|
rubocop (= 1.71.0)
|
||||||
|
rubocop-minitest (= 0.36.0)
|
||||||
|
rubocop-performance (= 1.23.1)
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.6.2
|
||||||
9
Makefile
9
Makefile
@@ -82,12 +82,15 @@ test: $(SOURCES)
|
|||||||
github.com/junegunn/fzf/src/tui \
|
github.com/junegunn/fzf/src/tui \
|
||||||
github.com/junegunn/fzf/src/util
|
github.com/junegunn/fzf/src/util
|
||||||
|
|
||||||
|
itest:
|
||||||
|
ruby test/runner.rb
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||||
|
|
||||||
lint: $(SOURCES) test/test_go.rb
|
lint: $(SOURCES) test/*.rb test/lib/*.rb
|
||||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||||
rubocop --require rubocop-minitest --require rubocop-performance
|
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
install: bin/fzf
|
install: bin/fzf
|
||||||
|
|
||||||
@@ -186,4 +189,4 @@ update:
|
|||||||
$(GO) get -u
|
$(GO) get -u
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
.PHONY: all generate build release test itest bench lint install clean docker docker-test update
|
||||||
|
|||||||
@@ -57,15 +57,15 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
|
|||||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1. Use kitty icat on kitty terminal
|
# 1. Use icat (from Kitty) if kitten is installed
|
||||||
if [[ $KITTY_WINDOW_ID ]]; then
|
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||||
# you have to use 'stream'.
|
# you have to use 'stream'.
|
||||||
#
|
#
|
||||||
# 2. The last line of the output is the ANSI reset code without newline.
|
# 2. The last line of the output is the ANSI reset code without newline.
|
||||||
# This confuses fzf and makes it render scroll offset indicator.
|
# This confuses fzf and makes it render scroll offset indicator.
|
||||||
# So we remove the last line and append the reset code to its previous line.
|
# So we remove the last line and append the reset code to its previous line.
|
||||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||||
|
|
||||||
# 2. Use chafa with Sixel output
|
# 2. Use chafa with Sixel output
|
||||||
elif command -v chafa > /dev/null; then
|
elif command -v chafa > /dev/null; then
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -3,11 +3,11 @@ module github.com/junegunn/fzf
|
|||||||
require (
|
require (
|
||||||
github.com/charlievieth/fastwalk v1.0.9
|
github.com/charlievieth/fastwalk v1.0.9
|
||||||
github.com/gdamore/tcell/v2 v2.8.1
|
github.com/gdamore/tcell/v2 v2.8.1
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/rivo/uniseg v0.4.7
|
github.com/rivo/uniseg v0.4.7
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/term v0.28.0
|
golang.org/x/term v0.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -5,8 +5,8 @@ github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeekl
|
|||||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -54,8 +54,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -64,8 +65,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|||||||
2
install
2
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.58.0
|
version=0.60.2
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.58.0"
|
$version="0.60.2"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$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"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.58"
|
var version = "0.60"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf\-tmux 1 "Jan 2025" "fzf 0.58.0" "fzf\-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "Feb 2025" "fzf 0.60.2" "fzf\-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf\-tmux - open fzf in tmux split pane
|
fzf\-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
122
man/man1/fzf.1
122
man/man1/fzf.1
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Jan 2025" "fzf 0.58.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "Feb 2025" "fzf 0.60.2" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -56,7 +56,9 @@ Case-insensitive match (default: smart-case match)
|
|||||||
Case-sensitive match
|
Case-sensitive match
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-smart\-case"
|
.B "\-\-smart\-case"
|
||||||
Smart-case match (default)
|
Smart-case match (default). In this mode, the search is case-insensitive by
|
||||||
|
default, but it becomes case-sensitive if the query contains any uppercase
|
||||||
|
letters.
|
||||||
.TP
|
.TP
|
||||||
.B "\-\-literal"
|
.B "\-\-literal"
|
||||||
Do not normalize latin script letters for matching.
|
Do not normalize latin script letters for matching.
|
||||||
@@ -76,7 +78,8 @@ Generic scoring scheme designed to work well with any type of input.
|
|||||||
.RS
|
.RS
|
||||||
Additional bonus point is only given to the characters after path separator.
|
Additional bonus point is only given to the characters after path separator.
|
||||||
You might want to choose this scheme over \fBdefault\fR if you have many files
|
You might want to choose this scheme over \fBdefault\fR if you have many files
|
||||||
with spaces in their paths.
|
with spaces in their paths. This also sets \fB\-\-tiebreak=pathname,length\fR,
|
||||||
|
to prioritize matches occurring in the tail element of a file path.
|
||||||
.RE
|
.RE
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
@@ -90,6 +93,13 @@ more weight to the chronological ordering. This also sets
|
|||||||
.RE
|
.RE
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
|
.RS
|
||||||
|
fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would
|
||||||
|
start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no
|
||||||
|
\fBreload\fR or \fBtransform\fR action bound to \fBstart\fR event. Otherwise,
|
||||||
|
it chooses \fBdefault\fR scheme.
|
||||||
|
.RE
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-algo=" TYPE
|
.BI "\-\-algo=" TYPE
|
||||||
Fuzzy matching algorithm (default: v2)
|
Fuzzy matching algorithm (default: v2)
|
||||||
@@ -109,8 +119,38 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
|
|||||||
the original lines) because fzf doesn't allow searching against the hidden
|
the original lines) because fzf doesn't allow searching against the hidden
|
||||||
fields.
|
fields.
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-with\-nth=" "N[,..]"
|
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||||
Transform the presentation of each line using field index expressions
|
Transform the presentation of each line using the field index expressions.
|
||||||
|
For advanced transformation, you can provide a template containing field index
|
||||||
|
expressions in curly braces. When you use a template, the trailing delimiter is
|
||||||
|
stripped from each expression, giving you more control over the output.
|
||||||
|
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
|
||||||
|
|
||||||
|
.RS
|
||||||
|
e.g.
|
||||||
|
# Single expression: drop the first field
|
||||||
|
echo foo bar baz | fzf --with-nth 2..
|
||||||
|
|
||||||
|
# Use template to rearrange fields
|
||||||
|
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||||
|
Define which fields to print on accept. The last delimiter is stripped from the
|
||||||
|
output. For advanced transformation, you can provide a template containing
|
||||||
|
field index expressions in curly braces. When you use a template, the trailing
|
||||||
|
delimiter is stripped from each expression, giving you more control over the
|
||||||
|
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
|
||||||
|
line.
|
||||||
|
|
||||||
|
.RS
|
||||||
|
e.g.
|
||||||
|
# Single expression
|
||||||
|
echo foo bar baz | fzf --accept-nth 2
|
||||||
|
|
||||||
|
# Template
|
||||||
|
echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
|
||||||
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.B "+s, \-\-no\-sort"
|
.B "+s, \-\-no\-sort"
|
||||||
Do not sort the result
|
Do not sort the result
|
||||||
@@ -144,6 +184,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
|||||||
.br
|
.br
|
||||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||||
.br
|
.br
|
||||||
|
.BR pathname " Prefers line with matched substring in the file name of the path"
|
||||||
|
.br
|
||||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||||
.br
|
.br
|
||||||
.BR end " Prefers line with matched substring closer to the end"
|
.BR end " Prefers line with matched substring closer to the end"
|
||||||
@@ -326,9 +368,12 @@ Adaptive height has the following limitations:
|
|||||||
* It will not find the right size when there are multi-line items
|
* It will not find the right size when there are multi-line items
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-min\-height=" "HEIGHT"
|
.BI "\-\-min\-height=" "HEIGHT[+]"
|
||||||
Minimum height when \fB\-\-height\fR is given in percent (default: 10).
|
Minimum height when \fB\-\-height\fR is given as a percentage.
|
||||||
Ignored when \fB\-\-height\fR is not specified.
|
Add \fB+\fR to automatically increase the value according to the other
|
||||||
|
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
|
.TP
|
||||||
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
|
.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
|
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
|
||||||
@@ -607,6 +652,13 @@ Position of the list label
|
|||||||
|
|
||||||
.SS INPUT SECTION
|
.SS INPUT SECTION
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B "\-\-no\-input"
|
||||||
|
Disable and hide the input section. You can no longer type in queries. To
|
||||||
|
trigger a search, use \fBsearch\fR action. You can later show the input section
|
||||||
|
using \fBshow\-input\fR or \fBtoggle\-input\fR action, and hide it again using
|
||||||
|
\fBhide\-input\fR, or \fBtoggle\-input\fR.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-prompt=" "STR"
|
.BI "\-\-prompt=" "STR"
|
||||||
Input prompt (default: '> ')
|
Input prompt (default: '> ')
|
||||||
@@ -730,6 +782,8 @@ Also,
|
|||||||
|
|
||||||
* \fB{q}\fR is replaced to the current query string
|
* \fB{q}\fR is replaced to the current query string
|
||||||
.br
|
.br
|
||||||
|
* \fB{q}\fR can contain field index expressions. e.g. \fB{q:1}\fR, \fB{q:2..}\fR, etc.
|
||||||
|
.br
|
||||||
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
||||||
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||||
.br
|
.br
|
||||||
@@ -929,6 +983,12 @@ Label to print on the header border
|
|||||||
.BI "\-\-header\-label\-pos" [=N[:top|bottom]]
|
.BI "\-\-header\-label\-pos" [=N[:top|bottom]]
|
||||||
Position of the header label
|
Position of the header label
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-header\-lines\-border" [=STYLE]
|
||||||
|
Display header from \fB--header\-lines\fR with a separate border. Pass
|
||||||
|
\fBnone\fR to still separate the header lines but without a border. To combine
|
||||||
|
two headers, use \fB\-\-no\-header\-lines\-border\fR.
|
||||||
|
|
||||||
.SS SCRIPTING
|
.SS SCRIPTING
|
||||||
.TP
|
.TP
|
||||||
.BI "\-q, \-\-query=" "STR"
|
.BI "\-q, \-\-query=" "STR"
|
||||||
@@ -1122,9 +1182,9 @@ Show man page
|
|||||||
.SH ENVIRONMENT VARIABLES
|
.SH ENVIRONMENT VARIABLES
|
||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_COMMAND
|
.B FZF_DEFAULT_COMMAND
|
||||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
Default command to use when input is a TTY device. On *nix systems, fzf runs
|
||||||
with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh \-c\fR, so in
|
the command with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh
|
||||||
this case make sure that the command is POSIX-compliant.
|
\-c\fR, so in this case make sure that the command is POSIX-compliant.
|
||||||
.TP
|
.TP
|
||||||
.B FZF_DEFAULT_OPTS
|
.B FZF_DEFAULT_OPTS
|
||||||
Default options.
|
Default options.
|
||||||
@@ -1196,6 +1256,8 @@ fzf exports the following environment variables to its child processes.
|
|||||||
.br
|
.br
|
||||||
.BR FZF_QUERY " Current query string"
|
.BR FZF_QUERY " Current query string"
|
||||||
.br
|
.br
|
||||||
|
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
|
||||||
|
.br
|
||||||
.BR FZF_NTH " Current \-\-nth option"
|
.BR FZF_NTH " Current \-\-nth option"
|
||||||
.br
|
.br
|
||||||
.BR FZF_PROMPT " Prompt string"
|
.BR FZF_PROMPT " Prompt string"
|
||||||
@@ -1275,10 +1337,15 @@ more \fBactions\fR. You can use it to customize key bindings or implement
|
|||||||
dynamic behaviors.
|
dynamic behaviors.
|
||||||
|
|
||||||
\fB\-\-bind\fR takes a comma-separated list of binding expressions. Each binding
|
\fB\-\-bind\fR takes a comma-separated list of binding expressions. Each binding
|
||||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
|
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. You can bind actions to
|
||||||
|
multiple keys and events by writing comma-separated list of keys and events
|
||||||
|
before the colon. e.g. \fBKEY1,KEY2,EVENT1,EVENT2:ACTION\fR.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line\fR
|
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line
|
||||||
|
|
||||||
|
# Load 'ps \-ef' output on start and reload it on CTRL\-R
|
||||||
|
fzf \-\-bind 'start,ctrl\-r:reload:ps \-ef'\fR
|
||||||
|
|
||||||
.SS AVAILABLE KEYS: (SYNONYMS)
|
.SS AVAILABLE KEYS: (SYNONYMS)
|
||||||
\fIctrl\-[a\-z]\fR
|
\fIctrl\-[a\-z]\fR
|
||||||
@@ -1503,11 +1570,21 @@ e.g.
|
|||||||
|
|
||||||
\fIclick\-header\fR
|
\fIclick\-header\fR
|
||||||
.RS
|
.RS
|
||||||
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
|
Triggered when a mouse click occurs within the header. Sets
|
||||||
|
\fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment
|
||||||
|
variables starting from 1. It optionally sets \fBFZF_CLICK_HEADER_WORD\fR and
|
||||||
|
\fBFZF_CLICK_HEADER_NTH\fR if clicked on a word.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fBprintf "head1\\nhead2" | fzf \-\-header\-lines=2 \-\-bind 'click\-header:transform\-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
|
\fB# Click on the header line to limit search scope
|
||||||
|
ps \-ef | fzf \-\-style full \-\-layout reverse \-\-header\-lines 1 \\
|
||||||
|
\-\-header\-lines\-border bottom \-\-no\-list\-border \\
|
||||||
|
\-\-color fg:dim,nth:regular \\
|
||||||
|
\-\-bind 'click\-header:transform\-nth(
|
||||||
|
echo $FZF_CLICK_HEADER_NTH
|
||||||
|
)+transform\-prompt(
|
||||||
|
echo "$FZF_CLICK_HEADER_WORD> "
|
||||||
|
)'\fR
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
.SS AVAILABLE ACTIONS:
|
.SS AVAILABLE ACTIONS:
|
||||||
@@ -1525,6 +1602,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBbackward\-word\fR \fIalt\-b shift\-left\fR
|
\fBbackward\-word\fR \fIalt\-b shift\-left\fR
|
||||||
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||||
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
|
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
|
||||||
|
\fBbell\fR (ring the terminal bell)
|
||||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||||
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
||||||
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
||||||
@@ -1551,6 +1629,8 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||||
\fBenable\-search\fR (enable search functionality)
|
\fBenable\-search\fR (enable search functionality)
|
||||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||||
|
\fBexclude\fR (exclude the current item from the result)
|
||||||
|
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
|
||||||
\fBexecute(...)\fR (see below for the details)
|
\fBexecute(...)\fR (see below for the details)
|
||||||
\fBexecute\-silent(...)\fR (see below for the details)
|
\fBexecute\-silent(...)\fR (see below for the details)
|
||||||
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
||||||
@@ -1568,6 +1648,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBhalf\-page\-down\fR
|
\fBhalf\-page\-down\fR
|
||||||
\fBhalf\-page\-up\fR
|
\fBhalf\-page\-up\fR
|
||||||
\fBhide\-header\fR
|
\fBhide\-header\fR
|
||||||
|
\fBhide\-input\fR
|
||||||
\fBhide\-preview\fR
|
\fBhide\-preview\fR
|
||||||
\fBoffset\-down\fR (similar to CTRL\-E of Vim)
|
\fBoffset\-down\fR (similar to CTRL\-E of Vim)
|
||||||
\fBoffset\-up\fR (similar to CTRL\-Y of Vim)
|
\fBoffset\-up\fR (similar to CTRL\-Y of Vim)
|
||||||
@@ -1592,16 +1673,20 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBreload(...)\fR (see below for the details)
|
\fBreload(...)\fR (see below for the details)
|
||||||
\fBreload\-sync(...)\fR (see below for the details)
|
\fBreload\-sync(...)\fR (see below for the details)
|
||||||
\fBreplace\-query\fR (replace query string with the current selection)
|
\fBreplace\-query\fR (replace query string with the current selection)
|
||||||
|
\fBsearch(...)\fR (trigger fzf search with the given string)
|
||||||
\fBselect\fR
|
\fBselect\fR
|
||||||
\fBselect\-all\fR (select all matches)
|
\fBselect\-all\fR (select all matches)
|
||||||
\fBshow\-header\fR
|
\fBshow\-header\fR
|
||||||
|
\fBshow\-input\fR
|
||||||
\fBshow\-preview\fR
|
\fBshow\-preview\fR
|
||||||
\fBtoggle\fR (\fIright\-click\fR)
|
\fBtoggle\fR (\fIright\-click\fR)
|
||||||
\fBtoggle\-all\fR (toggle all matches)
|
\fBtoggle\-all\fR (toggle all matches)
|
||||||
\fBtoggle\-in\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
\fBtoggle\-in\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||||
\fBtoggle\-out\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
\fBtoggle\-out\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||||
|
\fBtoggle\-bind\fR
|
||||||
\fBtoggle\-header\fR
|
\fBtoggle\-header\fR
|
||||||
\fBtoggle\-hscroll\fR
|
\fBtoggle\-hscroll\fR
|
||||||
|
\fBtoggle\-input\fR
|
||||||
\fBtoggle\-multi\-line\fR
|
\fBtoggle\-multi\-line\fR
|
||||||
\fBtoggle\-preview\fR
|
\fBtoggle\-preview\fR
|
||||||
\fBtoggle\-preview\-wrap\fR
|
\fBtoggle\-preview\-wrap\fR
|
||||||
@@ -1619,9 +1704,11 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
||||||
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
||||||
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
||||||
|
\fBtransform\-nth(...)\fR (transform nth using an external command)
|
||||||
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
|
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
|
||||||
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
|
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
|
||||||
\fBtransform\-query(...)\fR (transform query string using an external command)
|
\fBtransform\-query(...)\fR (transform query string using an external command)
|
||||||
|
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
|
||||||
\fBunbind(...)\fR (unbind bindings)
|
\fBunbind(...)\fR (unbind bindings)
|
||||||
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
||||||
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
||||||
@@ -1637,6 +1724,9 @@ e.g.
|
|||||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
|
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
|
||||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
|
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
|
||||||
|
|
||||||
|
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
|
||||||
|
\fBabort\fR, is ignored.
|
||||||
|
|
||||||
.SS ACTION ARGUMENT
|
.SS ACTION ARGUMENT
|
||||||
|
|
||||||
An action denoted with \fB(...)\fR suffix takes an argument.
|
An action denoted with \fB(...)\fR suffix takes an argument.
|
||||||
|
|||||||
@@ -1081,7 +1081,7 @@ endfunction
|
|||||||
|
|
||||||
function! s:cmd(bang, ...) abort
|
function! s:cmd(bang, ...) abort
|
||||||
let args = copy(a:000)
|
let args = copy(a:000)
|
||||||
let opts = { 'options': ['--multi'] }
|
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||||
if len(args) && isdirectory(expand(args[-1]))
|
if len(args) && isdirectory(expand(args[-1]))
|
||||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||||
if s:is_win && !&shellslash
|
if s:is_win && !&shellslash
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ bind '"\e[0n": redraw-current-line' 2> /dev/null
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,33 @@ _fzf_complete_kill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_proc_completion() {
|
_fzf_proc_completion() {
|
||||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
local transformer
|
||||||
|
transformer='
|
||||||
|
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||||
|
nths=( ${FZF_NTH//,/ } )
|
||||||
|
new_nths=()
|
||||||
|
found=0
|
||||||
|
for nth in ${nths[@]}; do
|
||||||
|
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||||
|
found=1
|
||||||
|
else
|
||||||
|
new_nths+=($nth)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||||
|
new_nths=${new_nths[*]}
|
||||||
|
new_nths=${new_nths// /,}
|
||||||
|
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||||
|
else
|
||||||
|
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||||
|
echo "change-nth()+change-prompt(> )"
|
||||||
|
else
|
||||||
|
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||||
|
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||||
command ps --everyone --full --windows # For cygwin
|
command ps --everyone --full --windows # For cygwin
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_comprun() {
|
__fzf_comprun() {
|
||||||
@@ -290,7 +290,33 @@ _fzf_complete_unalias() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_kill() {
|
_fzf_complete_kill() {
|
||||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
local transformer
|
||||||
|
transformer='
|
||||||
|
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||||
|
nths=( ${FZF_NTH//,/ } )
|
||||||
|
new_nths=()
|
||||||
|
found=0
|
||||||
|
for nth in ${nths[@]}; do
|
||||||
|
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||||
|
found=1
|
||||||
|
else
|
||||||
|
new_nths+=($nth)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||||
|
new_nths=${new_nths[*]}
|
||||||
|
new_nths=${new_nths// /,}
|
||||||
|
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||||
|
else
|
||||||
|
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||||
|
echo "change-nth()+change-prompt(> )"
|
||||||
|
else
|
||||||
|
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||||
|
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||||
command ps --everyone --full --windows # For cygwin
|
command ps --everyone --full --windows # For cygwin
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if [[ $- =~ i ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,77 @@
|
|||||||
|
|
||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
|
# For compatibility with fish versions down to 3.1.2, the script does not use:
|
||||||
|
# - The -f/--function switch of command: set
|
||||||
|
# - The process substitution syntax: $(cmd)
|
||||||
|
# - Ranges that omit start/end indexes: $var[$start..] $var[..$end] $var[..]
|
||||||
function fzf_key_bindings
|
function fzf_key_bindings
|
||||||
|
|
||||||
function __fzf_defaults
|
function __fzf_defaults
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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 FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
string join ' ' -- \
|
||||||
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
(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_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l dir '.'
|
||||||
|
set -l query
|
||||||
|
set -l commandline (commandline -t | string unescape -n)
|
||||||
|
|
||||||
|
# Strip -option= from token if present
|
||||||
|
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||||
|
set commandline (string replace -- "$prefix" '' $commandline)
|
||||||
|
|
||||||
|
# Enable home directory expansion of leading ~/
|
||||||
|
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
|
||||||
|
|
||||||
|
# Escape special characters, except for the $ sign of valid variable names,
|
||||||
|
# so that the original string with expanded variables is returned after eval.
|
||||||
|
set commandline (string escape -n -- $commandline)
|
||||||
|
set commandline (string replace -r -a -- '\\\\\$(?=[\w])' '\$' $commandline)
|
||||||
|
|
||||||
|
# eval is used to do shell expansion on paths
|
||||||
|
eval set commandline $commandline
|
||||||
|
|
||||||
|
# Combine multiple consecutive slashes into one.
|
||||||
|
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
||||||
|
|
||||||
|
if test -n "$commandline"
|
||||||
|
# Strip trailing slash, unless $dir is root dir (/)
|
||||||
|
set dir (string replace -r -- '(?<!^)/$' '' $commandline)
|
||||||
|
|
||||||
|
# Set $dir to the longest existing filepath
|
||||||
|
while not test -d "$dir"
|
||||||
|
# If path is absolute, this can keep going until ends up at /
|
||||||
|
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||||
|
set dir (dirname -- $dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
if test "$dir" = '.'; and test (string sub -l 2 -- $commandline) != './'
|
||||||
|
# If $dir is "." but commandline is not a relative path, this means no file path found
|
||||||
|
set fzf_query $commandline
|
||||||
|
else
|
||||||
|
# Also remove trailing slash after dir, to "split" input properly
|
||||||
|
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Store current token in $dir as root for the 'find' command
|
# Store current token in $dir as root for the 'find' command
|
||||||
@@ -31,40 +93,37 @@ function fzf_key_bindings
|
|||||||
set -lx dir $commandline[1]
|
set -lx dir $commandline[1]
|
||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
set -l prefix $commandline[3]
|
set -l prefix $commandline[3]
|
||||||
set -l result
|
|
||||||
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
begin
|
"--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" \
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS")
|
"$FZF_CTRL_T_OPTS --multi")
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
set result (eval (__fzfcmd) -m --query=$fzf_query)
|
|
||||||
end
|
if set -l result (eval (__fzfcmd) --query=$fzf_query)
|
||||||
if test -z "$result"
|
|
||||||
commandline -f repaint
|
|
||||||
return
|
|
||||||
else
|
|
||||||
# Remove last token from commandline.
|
# Remove last token from commandline.
|
||||||
commandline -t ""
|
commandline -t ''
|
||||||
end
|
|
||||||
for i in $result
|
for i in $result
|
||||||
commandline -it -- $prefix
|
commandline -it -- $prefix(string escape -- $i)' '
|
||||||
commandline -it -- (string escape -- $i)
|
|
||||||
commandline -it -- ' '
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function fzf-history-widget -d "Show command history"
|
function fzf-history-widget -d "Show command history"
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -l fzf_query (commandline | string escape)
|
||||||
begin
|
|
||||||
# merge history from other sessions before searching
|
|
||||||
test -z "$fish_private_mode"; and builtin history merge
|
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line +m $FZF_CTRL_R_OPTS")
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
'--nth=2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign="\t↳ "' \
|
||||||
|
"--highlight-line --no-multi $FZF_CTRL_R_OPTS --read0 --print0" \
|
||||||
|
"--bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'" \
|
||||||
|
'--with-shell='(status fish-path)\\ -c)
|
||||||
|
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
set -lx FZF_DEFAULT_COMMAND
|
set -lx FZF_DEFAULT_COMMAND
|
||||||
string match -q -r -- '/fish$' $SHELL; or set -lx SHELL (type -p fish)
|
|
||||||
if type -q perl
|
if type -q perl
|
||||||
set -a FZF_DEFAULT_OPTS '--tac'
|
set -a FZF_DEFAULT_OPTS '--tac'
|
||||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||||
@@ -75,9 +134,13 @@ function fzf_key_bindings
|
|||||||
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
||||||
'end'
|
'end'
|
||||||
end
|
end
|
||||||
set -l result (eval "$FZF_DEFAULT_COMMAND | $(__fzfcmd) --read0 --print0 -q (commandline) --bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'")
|
|
||||||
|
# Merge history from other sessions before searching
|
||||||
|
test -z "$fish_private_mode"; and builtin history merge
|
||||||
|
|
||||||
|
set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query)
|
||||||
and commandline -- $result
|
and commandline -- $result
|
||||||
end
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,113 +150,32 @@ function fzf_key_bindings
|
|||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
set -l prefix $commandline[3]
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
begin
|
"--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" \
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS")
|
"$FZF_ALT_C_OPTS --no-multi")
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||||
set -l result (eval (__fzfcmd) +m --query=$fzf_query)
|
|
||||||
|
|
||||||
if test -n "$result"
|
if set -l result (eval (__fzfcmd) --query=$fzf_query)
|
||||||
cd -- $result
|
cd -- $result
|
||||||
|
commandline -rt -- $prefix
|
||||||
# Remove last token from commandline.
|
|
||||||
commandline -t ""
|
|
||||||
commandline -it -- $prefix
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzfcmd
|
|
||||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set 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
|
|
||||||
|
|
||||||
bind \cr fzf-history-widget
|
bind \cr fzf-history-widget
|
||||||
|
bind -M insert \cr fzf-history-widget
|
||||||
|
|
||||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||||
bind \ct fzf-file-widget
|
bind \ct fzf-file-widget
|
||||||
end
|
|
||||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
|
||||||
bind \ec fzf-cd-widget
|
|
||||||
end
|
|
||||||
|
|
||||||
bind -M insert \cr fzf-history-widget
|
|
||||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
|
||||||
bind -M insert \ct fzf-file-widget
|
bind -M insert \ct fzf-file-widget
|
||||||
end
|
end
|
||||||
|
|
||||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||||
|
bind \ec fzf-cd-widget
|
||||||
bind -M insert \ec fzf-cd-widget
|
bind -M insert \ec fzf-cd-widget
|
||||||
end
|
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 commandline (commandline -t)
|
|
||||||
|
|
||||||
# strip -option= from token if present
|
|
||||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
|
||||||
set commandline (string replace -- "$prefix" '' $commandline)
|
|
||||||
|
|
||||||
# Enable home directory expansion of leading ~/
|
|
||||||
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
|
|
||||||
|
|
||||||
# escape special characters, except for the $ sign of valid variable names,
|
|
||||||
# so that after eval, the original string is returned, but with the
|
|
||||||
# variable names replaced by their values.
|
|
||||||
set commandline (string escape -n -- $commandline)
|
|
||||||
set commandline (string replace -r -a -- '\x5c\$(?=[\w])' '\$' $commandline)
|
|
||||||
|
|
||||||
# eval is used to do shell expansion on paths
|
|
||||||
eval set commandline $commandline
|
|
||||||
|
|
||||||
# Combine multiple consecutive slashes into one
|
|
||||||
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
|
||||||
|
|
||||||
if test -z "$commandline"
|
|
||||||
# Default to current directory with no --query
|
|
||||||
set dir '.'
|
|
||||||
set fzf_query ''
|
|
||||||
else
|
|
||||||
set dir (__fzf_get_dir $commandline)
|
|
||||||
|
|
||||||
# BUG: on combined expressions, if a left argument is a single `!`, the
|
|
||||||
# builtin test command of fish will treat it as the ! operator. To
|
|
||||||
# overcome this, have the variable parts on the right.
|
|
||||||
if test "." = "$dir" -a "./" != (string sub -l 2 -- $commandline)
|
|
||||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
|
||||||
set fzf_query $commandline
|
|
||||||
else
|
|
||||||
# Also remove trailing slash after dir, to "split" input properly
|
|
||||||
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
echo (string escape -- $dir)
|
|
||||||
echo (string escape -- $fzf_query)
|
|
||||||
echo $prefix
|
|
||||||
end
|
|
||||||
|
|
||||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
|
||||||
set dir $argv
|
|
||||||
|
|
||||||
# Strip trailing slash, unless $dir is root dir (/)
|
|
||||||
set dir (string replace -r -- '(?<!^)/$' '' $dir)
|
|
||||||
|
|
||||||
# Iteratively check if dir exists and strip tail end of path
|
|
||||||
while test ! -d "$dir"
|
|
||||||
# If path is absolute, this can keep going until ends up at /
|
|
||||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
|
||||||
set dir (dirname -- "$dir")
|
|
||||||
end
|
|
||||||
|
|
||||||
echo $dir
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|
||||||
# CTRL-T - Paste the selected file path(s) into the command line
|
# CTRL-T - Paste the selected file path(s) into the command line
|
||||||
|
|||||||
@@ -66,75 +66,85 @@ func _() {
|
|||||||
_ = x[actToggleMultiLine-55]
|
_ = x[actToggleMultiLine-55]
|
||||||
_ = x[actToggleHscroll-56]
|
_ = x[actToggleHscroll-56]
|
||||||
_ = x[actTrackCurrent-57]
|
_ = x[actTrackCurrent-57]
|
||||||
_ = x[actUntrackCurrent-58]
|
_ = x[actToggleInput-58]
|
||||||
_ = x[actDown-59]
|
_ = x[actHideInput-59]
|
||||||
_ = x[actUp-60]
|
_ = x[actShowInput-60]
|
||||||
_ = x[actPageUp-61]
|
_ = x[actUntrackCurrent-61]
|
||||||
_ = x[actPageDown-62]
|
_ = x[actDown-62]
|
||||||
_ = x[actPosition-63]
|
_ = x[actUp-63]
|
||||||
_ = x[actHalfPageUp-64]
|
_ = x[actPageUp-64]
|
||||||
_ = x[actHalfPageDown-65]
|
_ = x[actPageDown-65]
|
||||||
_ = x[actOffsetUp-66]
|
_ = x[actPosition-66]
|
||||||
_ = x[actOffsetDown-67]
|
_ = x[actHalfPageUp-67]
|
||||||
_ = x[actOffsetMiddle-68]
|
_ = x[actHalfPageDown-68]
|
||||||
_ = x[actJump-69]
|
_ = x[actOffsetUp-69]
|
||||||
_ = x[actJumpAccept-70]
|
_ = x[actOffsetDown-70]
|
||||||
_ = x[actPrintQuery-71]
|
_ = x[actOffsetMiddle-71]
|
||||||
_ = x[actRefreshPreview-72]
|
_ = x[actJump-72]
|
||||||
_ = x[actReplaceQuery-73]
|
_ = x[actJumpAccept-73]
|
||||||
_ = x[actToggleSort-74]
|
_ = x[actPrintQuery-74]
|
||||||
_ = x[actShowPreview-75]
|
_ = x[actRefreshPreview-75]
|
||||||
_ = x[actHidePreview-76]
|
_ = x[actReplaceQuery-76]
|
||||||
_ = x[actTogglePreview-77]
|
_ = x[actToggleSort-77]
|
||||||
_ = x[actTogglePreviewWrap-78]
|
_ = x[actShowPreview-78]
|
||||||
_ = x[actTransform-79]
|
_ = x[actHidePreview-79]
|
||||||
_ = x[actTransformBorderLabel-80]
|
_ = x[actTogglePreview-80]
|
||||||
_ = x[actTransformListLabel-81]
|
_ = x[actTogglePreviewWrap-81]
|
||||||
_ = x[actTransformInputLabel-82]
|
_ = x[actTransform-82]
|
||||||
_ = x[actTransformHeader-83]
|
_ = x[actTransformBorderLabel-83]
|
||||||
_ = x[actTransformHeaderLabel-84]
|
_ = x[actTransformListLabel-84]
|
||||||
_ = x[actTransformPreviewLabel-85]
|
_ = x[actTransformInputLabel-85]
|
||||||
_ = x[actTransformPrompt-86]
|
_ = x[actTransformHeader-86]
|
||||||
_ = x[actTransformQuery-87]
|
_ = x[actTransformHeaderLabel-87]
|
||||||
_ = x[actPreview-88]
|
_ = x[actTransformNth-88]
|
||||||
_ = x[actChangePreview-89]
|
_ = x[actTransformPreviewLabel-89]
|
||||||
_ = x[actChangePreviewWindow-90]
|
_ = x[actTransformPrompt-90]
|
||||||
_ = x[actPreviewTop-91]
|
_ = x[actTransformQuery-91]
|
||||||
_ = x[actPreviewBottom-92]
|
_ = x[actTransformSearch-92]
|
||||||
_ = x[actPreviewUp-93]
|
_ = x[actSearch-93]
|
||||||
_ = x[actPreviewDown-94]
|
_ = x[actPreview-94]
|
||||||
_ = x[actPreviewPageUp-95]
|
_ = x[actChangePreview-95]
|
||||||
_ = x[actPreviewPageDown-96]
|
_ = x[actChangePreviewWindow-96]
|
||||||
_ = x[actPreviewHalfPageUp-97]
|
_ = x[actPreviewTop-97]
|
||||||
_ = x[actPreviewHalfPageDown-98]
|
_ = x[actPreviewBottom-98]
|
||||||
_ = x[actPrevHistory-99]
|
_ = x[actPreviewUp-99]
|
||||||
_ = x[actPrevSelected-100]
|
_ = x[actPreviewDown-100]
|
||||||
_ = x[actPrint-101]
|
_ = x[actPreviewPageUp-101]
|
||||||
_ = x[actPut-102]
|
_ = x[actPreviewPageDown-102]
|
||||||
_ = x[actNextHistory-103]
|
_ = x[actPreviewHalfPageUp-103]
|
||||||
_ = x[actNextSelected-104]
|
_ = x[actPreviewHalfPageDown-104]
|
||||||
_ = x[actExecute-105]
|
_ = x[actPrevHistory-105]
|
||||||
_ = x[actExecuteSilent-106]
|
_ = x[actPrevSelected-106]
|
||||||
_ = x[actExecuteMulti-107]
|
_ = x[actPrint-107]
|
||||||
_ = x[actSigStop-108]
|
_ = x[actPut-108]
|
||||||
_ = x[actFirst-109]
|
_ = x[actNextHistory-109]
|
||||||
_ = x[actLast-110]
|
_ = x[actNextSelected-110]
|
||||||
_ = x[actReload-111]
|
_ = x[actExecute-111]
|
||||||
_ = x[actReloadSync-112]
|
_ = x[actExecuteSilent-112]
|
||||||
_ = x[actDisableSearch-113]
|
_ = x[actExecuteMulti-113]
|
||||||
_ = x[actEnableSearch-114]
|
_ = x[actSigStop-114]
|
||||||
_ = x[actSelect-115]
|
_ = x[actFirst-115]
|
||||||
_ = x[actDeselect-116]
|
_ = x[actLast-116]
|
||||||
_ = x[actUnbind-117]
|
_ = x[actReload-117]
|
||||||
_ = x[actRebind-118]
|
_ = x[actReloadSync-118]
|
||||||
_ = x[actBecome-119]
|
_ = x[actDisableSearch-119]
|
||||||
_ = x[actShowHeader-120]
|
_ = x[actEnableSearch-120]
|
||||||
_ = x[actHideHeader-121]
|
_ = x[actSelect-121]
|
||||||
|
_ = x[actDeselect-122]
|
||||||
|
_ = x[actUnbind-123]
|
||||||
|
_ = x[actRebind-124]
|
||||||
|
_ = x[actToggleBind-125]
|
||||||
|
_ = x[actBecome-126]
|
||||||
|
_ = x[actShowHeader-127]
|
||||||
|
_ = x[actHideHeader-128]
|
||||||
|
_ = x[actBell-129]
|
||||||
|
_ = x[actExclude-130]
|
||||||
|
_ = x[actExcludeMulti-131]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformNthactTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
|
||||||
|
|
||||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1265, 1281, 1303, 1316, 1332, 1344, 1358, 1374, 1392, 1412, 1434, 1448, 1463, 1471, 1477, 1491, 1506, 1516, 1532, 1547, 1557, 1565, 1572, 1581, 1594, 1610, 1625, 1634, 1645, 1654, 1663, 1672, 1685, 1698}
|
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 822, 834, 846, 863, 870, 875, 884, 895, 906, 919, 934, 945, 958, 973, 980, 993, 1006, 1023, 1038, 1051, 1065, 1079, 1095, 1115, 1127, 1150, 1171, 1193, 1211, 1234, 1249, 1273, 1291, 1308, 1326, 1335, 1345, 1361, 1383, 1396, 1412, 1424, 1438, 1454, 1472, 1492, 1514, 1528, 1543, 1551, 1557, 1571, 1586, 1596, 1612, 1627, 1637, 1645, 1652, 1661, 1674, 1690, 1705, 1714, 1725, 1734, 1743, 1756, 1765, 1778, 1791, 1798, 1808, 1823}
|
||||||
|
|
||||||
func (i actionType) String() string {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
|||||||
char = unicode.To(unicode.LowerCase, char)
|
char = unicode.To(unicode.LowerCase, char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if normalize {
|
||||||
|
char = normalizeRune(char)
|
||||||
|
}
|
||||||
|
|
||||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||||
pchar := pattern[pidx_]
|
pchar := pattern[pidx_]
|
||||||
|
|||||||
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
|
|||||||
bytes[math.MaxUint16] = 'z'
|
bytes[math.MaxUint16] = 'z'
|
||||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLongStringWithNormalize(t *testing.T) {
|
||||||
|
bytes := make([]byte, 30000)
|
||||||
|
for i := range bytes {
|
||||||
|
bytes[i] = 'x'
|
||||||
|
}
|
||||||
|
unicodeString := string(bytes) + " Minímal example"
|
||||||
|
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||||
|
}
|
||||||
|
|||||||
13
src/ansi.go
13
src/ansi.go
@@ -358,12 +358,17 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||||
state.lbg = prevState.bg
|
state.lbg = prevState.bg
|
||||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
|
||||||
|
stLen := 2
|
||||||
|
if strings.HasSuffix(ansiCode, "\a") {
|
||||||
|
stLen = 1
|
||||||
|
}
|
||||||
|
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
|
||||||
|
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
|
||||||
state.url = nil
|
state.url = nil
|
||||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
|
||||||
params := ansiCode[4 : 4+paramsEnd]
|
params := ansiCode[4 : 4+paramsEnd]
|
||||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||||
state.url = &url{uri: uri, params: params}
|
state.url = &url{uri: uri, params: params}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/core.go
58
src/core.go
@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
header := make([]string, 0, opts.HeaderLines)
|
||||||
if len(opts.WithNth) == 0 {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, byteString(data))
|
header = append(header, byteString(data))
|
||||||
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||||
@@ -127,15 +128,13 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trans := Transform(tokens, opts.WithNth)
|
transformed := nthTransformer(tokens, itemIndex)
|
||||||
transformed := joinTokens(trans)
|
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, transformed)
|
header = append(header, transformed)
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||||
item.text.TrimTrailingWhitespaces()
|
|
||||||
item.text.Index = itemIndex
|
item.text.Index = itemIndex
|
||||||
item.origText = &data
|
item.origText = &data
|
||||||
itemIndex++
|
itemIndex++
|
||||||
@@ -188,19 +187,37 @@ func Run(opts *Options) (int, error) {
|
|||||||
forward = false
|
forward = false
|
||||||
case byBegin:
|
case byBegin:
|
||||||
forward = true
|
forward = true
|
||||||
|
case byPathname:
|
||||||
|
withPos = true
|
||||||
|
forward = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nth := opts.Nth
|
nth := opts.Nth
|
||||||
nthRevision := 0
|
|
||||||
patternCache := make(map[string]*Pattern)
|
|
||||||
patternBuilder := func(runes []rune) *Pattern {
|
|
||||||
return BuildPattern(cache, patternCache,
|
|
||||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
|
||||||
opts.Filter == nil, nth, opts.Delimiter, nthRevision, runes)
|
|
||||||
}
|
|
||||||
inputRevision := revision{}
|
inputRevision := revision{}
|
||||||
snapshotRevision := revision{}
|
snapshotRevision := revision{}
|
||||||
|
patternCache := make(map[string]*Pattern)
|
||||||
|
denyMutex := sync.Mutex{}
|
||||||
|
denylist := make(map[int32]struct{})
|
||||||
|
clearDenylist := func() {
|
||||||
|
denyMutex.Lock()
|
||||||
|
if len(denylist) > 0 {
|
||||||
|
patternCache = make(map[string]*Pattern)
|
||||||
|
}
|
||||||
|
denylist = make(map[int32]struct{})
|
||||||
|
denyMutex.Unlock()
|
||||||
|
}
|
||||||
|
patternBuilder := func(runes []rune) *Pattern {
|
||||||
|
denyMutex.Lock()
|
||||||
|
denylistCopy := make(map[int32]struct{})
|
||||||
|
for k, v := range denylist {
|
||||||
|
denylistCopy[k] = v
|
||||||
|
}
|
||||||
|
denyMutex.Unlock()
|
||||||
|
return BuildPattern(cache, patternCache,
|
||||||
|
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||||
|
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||||
|
}
|
||||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
@@ -299,6 +316,9 @@ func Run(opts *Options) (int, error) {
|
|||||||
var snapshot []*Chunk
|
var snapshot []*Chunk
|
||||||
var count int
|
var count int
|
||||||
restart := func(command commandSpec, environ []string) {
|
restart := func(command commandSpec, environ []string) {
|
||||||
|
if !useSnapshot {
|
||||||
|
clearDenylist()
|
||||||
|
}
|
||||||
reading = true
|
reading = true
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
@@ -345,7 +365,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
} else {
|
} else {
|
||||||
reading = reading && evt == EvtReadNew
|
reading = reading && evt == EvtReadNew
|
||||||
}
|
}
|
||||||
if useSnapshot && evt == EvtReadFin {
|
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||||
|
clearDenylist()
|
||||||
useSnapshot = false
|
useSnapshot = false
|
||||||
}
|
}
|
||||||
if !useSnapshot {
|
if !useSnapshot {
|
||||||
@@ -376,10 +397,21 @@ func Run(opts *Options) (int, error) {
|
|||||||
command = val.command
|
command = val.command
|
||||||
environ = val.environ
|
environ = val.environ
|
||||||
changed = val.changed
|
changed = val.changed
|
||||||
|
bump := false
|
||||||
|
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||||
|
denyMutex.Lock()
|
||||||
|
for _, itemIndex := range val.denylist {
|
||||||
|
denylist[itemIndex] = struct{}{}
|
||||||
|
}
|
||||||
|
denyMutex.Unlock()
|
||||||
|
bump = true
|
||||||
|
}
|
||||||
if val.nth != nil {
|
if val.nth != nil {
|
||||||
// Change nth and clear caches
|
// Change nth and clear caches
|
||||||
nth = *val.nth
|
nth = *val.nth
|
||||||
nthRevision++
|
bump = true
|
||||||
|
}
|
||||||
|
if bump {
|
||||||
patternCache = make(map[string]*Pattern)
|
patternCache = make(map[string]*Pattern)
|
||||||
cache.Clear()
|
cache.Clear()
|
||||||
inputRevision.bumpMinor()
|
inputRevision.bumpMinor()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
type transformed struct {
|
type transformed struct {
|
||||||
// Because nth can be changed dynamically by change-nth action, we need to
|
// Because nth can be changed dynamically by change-nth action, we need to
|
||||||
// keep the revision number at the time of transformation.
|
// keep the revision number at the time of transformation.
|
||||||
revision int
|
revision revision
|
||||||
tokens []Token
|
tokens []Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
314
src/options.go
314
src/options.go
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/junegunn/fzf/src/algo"
|
"github.com/junegunn/fzf/src/algo"
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
|
||||||
"github.com/junegunn/go-shellwords"
|
"github.com/junegunn/go-shellwords"
|
||||||
"github.com/rivo/uniseg"
|
"github.com/rivo/uniseg"
|
||||||
@@ -40,14 +41,15 @@ Usage: fzf [options]
|
|||||||
integer or a range expression ([BEGIN]..[END]).
|
integer or a range expression ([BEGIN]..[END]).
|
||||||
--with-nth=N[,..] Transform the presentation of each line using
|
--with-nth=N[,..] Transform the presentation of each line using
|
||||||
field index expressions
|
field index expressions
|
||||||
|
--accept-nth=N[,..] Define which fields to print on accept
|
||||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||||
+s, --no-sort Do not sort the result
|
+s, --no-sort Do not sort the result
|
||||||
--literal Do not normalize latin script letters
|
--literal Do not normalize latin script letters
|
||||||
--tail=NUM Maximum number of items to keep in memory
|
--tail=NUM Maximum number of items to keep in memory
|
||||||
--disabled Do not perform search
|
--disabled Do not perform search
|
||||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||||
when the scores are tied [length|chunk|begin|end|index]
|
when the scores are tied
|
||||||
(default: length)
|
[length|chunk|pathname|begin|end|index] (default: length)
|
||||||
|
|
||||||
INPUT/OUTPUT
|
INPUT/OUTPUT
|
||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
@@ -68,8 +70,9 @@ Usage: fzf [options]
|
|||||||
minus the given value.
|
minus the given value.
|
||||||
If prefixed with '~', fzf will determine the height
|
If prefixed with '~', fzf will determine the height
|
||||||
according to the input size.
|
according to the input size.
|
||||||
--min-height=HEIGHT Minimum height for percent --height is given in percent
|
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
|
||||||
(default: 10)
|
Add '+' to automatically increase the value
|
||||||
|
according to the other layout options (default: 10+).
|
||||||
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
|
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
|
||||||
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
[,border-native] (default: center,50%)
|
[,border-native] (default: center,50%)
|
||||||
@@ -125,6 +128,7 @@ Usage: fzf [options]
|
|||||||
(default: 0 or center)
|
(default: 0 or center)
|
||||||
|
|
||||||
INPUT SECTION
|
INPUT SECTION
|
||||||
|
--no-input Disable and hide the input section
|
||||||
--prompt=STR Input prompt (default: '> ')
|
--prompt=STR Input prompt (default: '> ')
|
||||||
--info=STYLE Finder info style
|
--info=STYLE Finder info style
|
||||||
[default|right|hidden|inline[-right][:PREFIX]]
|
[default|right|hidden|inline[-right][:PREFIX]]
|
||||||
@@ -164,6 +168,9 @@ Usage: fzf [options]
|
|||||||
--header-border[=STYLE] Draw border around the header section
|
--header-border[=STYLE] Draw border around the header section
|
||||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||||
top|bottom|left|right|none] (default: rounded)
|
top|bottom|left|right|none] (default: rounded)
|
||||||
|
--header-lines-border[=STYLE]
|
||||||
|
Display header from --header-lines with a separate border.
|
||||||
|
Pass 'none' to still separate it but without a border.
|
||||||
--header-label=LABEL Label to print on the header border
|
--header-label=LABEL Label to print on the header border
|
||||||
--header-label-pos=COL Position of the header label
|
--header-label-pos=COL Position of the header label
|
||||||
[POSITIVE_INTEGER: columns from left|
|
[POSITIVE_INTEGER: columns from left|
|
||||||
@@ -238,6 +245,7 @@ const (
|
|||||||
byLength
|
byLength
|
||||||
byBegin
|
byBegin
|
||||||
byEnd
|
byEnd
|
||||||
|
byPathname
|
||||||
)
|
)
|
||||||
|
|
||||||
type heightSpec struct {
|
type heightSpec struct {
|
||||||
@@ -532,10 +540,12 @@ type Options struct {
|
|||||||
Scheme string
|
Scheme string
|
||||||
Extended bool
|
Extended bool
|
||||||
Phony bool
|
Phony bool
|
||||||
|
Inputless bool
|
||||||
Case Case
|
Case Case
|
||||||
Normalize bool
|
Normalize bool
|
||||||
Nth []Range
|
Nth []Range
|
||||||
WithNth []Range
|
WithNth func(Delimiter) func([]Token, int32) string
|
||||||
|
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
Track trackOption
|
Track trackOption
|
||||||
@@ -597,6 +607,7 @@ type Options struct {
|
|||||||
ListBorderShape tui.BorderShape
|
ListBorderShape tui.BorderShape
|
||||||
InputBorderShape tui.BorderShape
|
InputBorderShape tui.BorderShape
|
||||||
HeaderBorderShape tui.BorderShape
|
HeaderBorderShape tui.BorderShape
|
||||||
|
HeaderLinesShape tui.BorderShape
|
||||||
InputLabel labelOpts
|
InputLabel labelOpts
|
||||||
HeaderLabel labelOpts
|
HeaderLabel labelOpts
|
||||||
BorderLabel labelOpts
|
BorderLabel labelOpts
|
||||||
@@ -649,25 +660,25 @@ func defaultOptions() *Options {
|
|||||||
Man: false,
|
Man: false,
|
||||||
Fuzzy: true,
|
Fuzzy: true,
|
||||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||||
Scheme: "default",
|
Scheme: "", // Unknown
|
||||||
Extended: true,
|
Extended: true,
|
||||||
Phony: false,
|
Phony: false,
|
||||||
|
Inputless: false,
|
||||||
Case: CaseSmart,
|
Case: CaseSmart,
|
||||||
Normalize: true,
|
Normalize: true,
|
||||||
Nth: make([]Range, 0),
|
Nth: make([]Range, 0),
|
||||||
WithNth: make([]Range, 0),
|
|
||||||
Delimiter: Delimiter{},
|
Delimiter: Delimiter{},
|
||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
Track: trackDisabled,
|
Track: trackDisabled,
|
||||||
Tac: false,
|
Tac: false,
|
||||||
Criteria: []criterion{byScore, byLength},
|
Criteria: []criterion{}, // Unknown
|
||||||
Multi: 0,
|
Multi: 0,
|
||||||
Ansi: false,
|
Ansi: false,
|
||||||
Mouse: true,
|
Mouse: true,
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
Black: false,
|
Black: false,
|
||||||
Bold: true,
|
Bold: true,
|
||||||
MinHeight: 10,
|
MinHeight: -10,
|
||||||
Layout: layoutDefault,
|
Layout: layoutDefault,
|
||||||
Cycle: false,
|
Cycle: false,
|
||||||
Wrap: false,
|
Wrap: false,
|
||||||
@@ -758,6 +769,70 @@ func splitNth(str string) ([]Range, error) {
|
|||||||
return ranges, nil
|
return ranges, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
|
||||||
|
// ^[0-9,-.]+$"
|
||||||
|
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
|
||||||
|
nth, err := splitNth(str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(Delimiter) func([]Token, int32) string {
|
||||||
|
return func(tokens []Token, index int32) string {
|
||||||
|
return JoinTokens(Transform(tokens, nth))
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// {...} {...} ...
|
||||||
|
placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
|
||||||
|
indexes := placeholder.FindAllStringIndex(str, -1)
|
||||||
|
if indexes == nil {
|
||||||
|
return nil, errors.New("template should include at least 1 placeholder: " + str)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NthParts struct {
|
||||||
|
str string
|
||||||
|
index bool
|
||||||
|
nth []Range
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]NthParts, len(indexes))
|
||||||
|
idx := 0
|
||||||
|
for _, index := range indexes {
|
||||||
|
if idx < index[0] {
|
||||||
|
parts = append(parts, NthParts{str: str[idx:index[0]]})
|
||||||
|
}
|
||||||
|
expr := str[index[0]+1 : index[1]-1]
|
||||||
|
if expr == "n" {
|
||||||
|
parts = append(parts, NthParts{index: true})
|
||||||
|
} else if nth, err := splitNth(expr); err == nil {
|
||||||
|
parts = append(parts, NthParts{nth: nth})
|
||||||
|
}
|
||||||
|
idx = index[1]
|
||||||
|
}
|
||||||
|
if idx < len(str) {
|
||||||
|
parts = append(parts, NthParts{str: str[idx:]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(delimiter Delimiter) func([]Token, int32) string {
|
||||||
|
return func(tokens []Token, index int32) string {
|
||||||
|
str := ""
|
||||||
|
for _, holder := range parts {
|
||||||
|
if holder.nth != nil {
|
||||||
|
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
|
||||||
|
} else if holder.index {
|
||||||
|
if index >= 0 {
|
||||||
|
str += strconv.Itoa(int(index))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str += holder.str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func delimiterRegexp(str string) Delimiter {
|
func delimiterRegexp(str string) Delimiter {
|
||||||
// Special handling of \t
|
// Special handling of \t
|
||||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||||
@@ -800,16 +875,6 @@ func parseAlgo(str string) (algo.Algo, error) {
|
|||||||
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
|
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processScheme(opts *Options) error {
|
|
||||||
if !algo.Init(opts.Scheme) {
|
|
||||||
return errors.New("invalid scoring scheme (expected: default|path|history)")
|
|
||||||
}
|
|
||||||
if opts.Scheme == "history" {
|
|
||||||
opts.Criteria = []criterion{byScore}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
|
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
|
||||||
switch str {
|
switch str {
|
||||||
case "line":
|
case "line":
|
||||||
@@ -885,7 +950,7 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
|||||||
case "right":
|
case "right":
|
||||||
add(tui.Right)
|
add(tui.Right)
|
||||||
case "enter", "return":
|
case "enter", "return":
|
||||||
add(tui.CtrlM)
|
add(tui.Enter)
|
||||||
case "space":
|
case "space":
|
||||||
chords[tui.Key(' ')] = key
|
chords[tui.Key(' ')] = key
|
||||||
case "backspace", "bspace", "bs":
|
case "backspace", "bspace", "bs":
|
||||||
@@ -1033,6 +1098,19 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
|||||||
return chords, nil
|
return chords, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseScheme(str string) (string, []criterion, error) {
|
||||||
|
str = strings.ToLower(str)
|
||||||
|
switch str {
|
||||||
|
case "history":
|
||||||
|
return str, []criterion{byScore}, nil
|
||||||
|
case "path":
|
||||||
|
return str, []criterion{byScore, byPathname, byLength}, nil
|
||||||
|
case "default":
|
||||||
|
return str, []criterion{byScore, byLength}, nil
|
||||||
|
}
|
||||||
|
return str, nil, errors.New("invalid scoring scheme: " + str + " (expected: default|path|history)")
|
||||||
|
}
|
||||||
|
|
||||||
func parseTiebreak(str string) ([]criterion, error) {
|
func parseTiebreak(str string) ([]criterion, error) {
|
||||||
criteria := []criterion{byScore}
|
criteria := []criterion{byScore}
|
||||||
hasIndex := false
|
hasIndex := false
|
||||||
@@ -1040,6 +1118,7 @@ func parseTiebreak(str string) ([]criterion, error) {
|
|||||||
hasLength := false
|
hasLength := false
|
||||||
hasBegin := false
|
hasBegin := false
|
||||||
hasEnd := false
|
hasEnd := false
|
||||||
|
hasPathname := false
|
||||||
check := func(notExpected *bool, name string) error {
|
check := func(notExpected *bool, name string) error {
|
||||||
if *notExpected {
|
if *notExpected {
|
||||||
return errors.New("duplicate sort criteria: " + name)
|
return errors.New("duplicate sort criteria: " + name)
|
||||||
@@ -1061,6 +1140,11 @@ func parseTiebreak(str string) ([]criterion, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
criteria = append(criteria, byChunk)
|
criteria = append(criteria, byChunk)
|
||||||
|
case "pathname":
|
||||||
|
if err := check(&hasPathname, "pathname"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
criteria = append(criteria, byPathname)
|
||||||
case "length":
|
case "length":
|
||||||
if err := check(&hasLength, "length"); err != nil {
|
if err := check(&hasLength, "length"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1317,7 +1401,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
executeRegexp = regexp.MustCompile(
|
executeRegexp = regexp.MustCompile(
|
||||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print)`)
|
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`)
|
||||||
splitRegexp = regexp.MustCompile("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
@@ -1372,6 +1456,8 @@ Loop:
|
|||||||
masked += strings.Repeat(" ", loc[1])
|
masked += strings.Repeat(" ", loc[1])
|
||||||
action = action[loc[1]:]
|
action = action[loc[1]:]
|
||||||
}
|
}
|
||||||
|
masked = strings.ReplaceAll(masked, ",,,", string([]rune{',', escapedComma, ','}))
|
||||||
|
masked = strings.ReplaceAll(masked, ",:,", string([]rune{',', escapedColon, ','}))
|
||||||
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
|
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
|
||||||
masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
|
masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
|
||||||
masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
|
masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
|
||||||
@@ -1479,6 +1565,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actToggleTrack)
|
appendAction(actToggleTrack)
|
||||||
case "toggle-track-current":
|
case "toggle-track-current":
|
||||||
appendAction(actToggleTrackCurrent)
|
appendAction(actToggleTrackCurrent)
|
||||||
|
case "toggle-input":
|
||||||
|
appendAction(actToggleInput)
|
||||||
|
case "hide-input":
|
||||||
|
appendAction(actHideInput)
|
||||||
|
case "show-input":
|
||||||
|
appendAction(actShowInput)
|
||||||
case "toggle-header":
|
case "toggle-header":
|
||||||
appendAction(actToggleHeader)
|
appendAction(actToggleHeader)
|
||||||
case "toggle-wrap":
|
case "toggle-wrap":
|
||||||
@@ -1571,6 +1663,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
} else {
|
} else {
|
||||||
return nil, errors.New("unable to put non-printable character")
|
return nil, errors.New("unable to put non-printable character")
|
||||||
}
|
}
|
||||||
|
case "bell":
|
||||||
|
appendAction(actBell)
|
||||||
|
case "exclude":
|
||||||
|
appendAction(actExclude)
|
||||||
|
case "exclude-multi":
|
||||||
|
appendAction(actExcludeMulti)
|
||||||
default:
|
default:
|
||||||
t := isExecuteAction(specLower)
|
t := isExecuteAction(specLower)
|
||||||
if t == actIgnore {
|
if t == actIgnore {
|
||||||
@@ -1597,7 +1695,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
actions = append(actions, &action{t: t, a: actionArg})
|
actions = append(actions, &action{t: t, a: actionArg})
|
||||||
}
|
}
|
||||||
switch t {
|
switch t {
|
||||||
case actUnbind, actRebind:
|
case actUnbind, actRebind, actToggleBind:
|
||||||
if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil {
|
if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1621,23 +1719,29 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error {
|
|||||||
var err error
|
var err error
|
||||||
masked := maskActionContents(str)
|
masked := maskActionContents(str)
|
||||||
idx := 0
|
idx := 0
|
||||||
|
keys := []string{}
|
||||||
for _, pairStr := range strings.Split(masked, ",") {
|
for _, pairStr := range strings.Split(masked, ",") {
|
||||||
origPairStr := str[idx : idx+len(pairStr)]
|
origPairStr := str[idx : idx+len(pairStr)]
|
||||||
idx += len(pairStr) + 1
|
idx += len(pairStr) + 1
|
||||||
|
|
||||||
pair := strings.SplitN(pairStr, ":", 2)
|
pair := strings.SplitN(pairStr, ":", 2)
|
||||||
if len(pair) < 2 {
|
if len(pair[0]) == 0 {
|
||||||
return errors.New("bind action not specified: " + origPairStr)
|
return errors.New("key name required")
|
||||||
}
|
}
|
||||||
|
keys = append(keys, pair[0])
|
||||||
|
if len(pair) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, keyName := range keys {
|
||||||
var key tui.Event
|
var key tui.Event
|
||||||
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
|
if len(keyName) == 1 && keyName[0] == escapedColon {
|
||||||
key = tui.Key(':')
|
key = tui.Key(':')
|
||||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
|
} else if len(keyName) == 1 && keyName[0] == escapedComma {
|
||||||
key = tui.Key(',')
|
key = tui.Key(',')
|
||||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
|
} else if len(keyName) == 1 && keyName[0] == escapedPlus {
|
||||||
key = tui.Key('+')
|
key = tui.Key('+')
|
||||||
} else {
|
} else {
|
||||||
keys, err := parseKeyChordsImpl(pair[0], "key name required")
|
keys, err := parseKeyChordsImpl(keyName, "key name required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1753,11 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keys = keys[:0]
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
return errors.New("bind action not specified: " + strings.Join(keys, ", "))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,6 +1780,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actUnbind
|
return actUnbind
|
||||||
case "rebind":
|
case "rebind":
|
||||||
return actRebind
|
return actRebind
|
||||||
|
case "toggle-bind":
|
||||||
|
return actToggleBind
|
||||||
case "preview":
|
case "preview":
|
||||||
return actPreview
|
return actPreview
|
||||||
case "change-header":
|
case "change-header":
|
||||||
@@ -1723,10 +1834,16 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actTransformHeaderLabel
|
return actTransformHeaderLabel
|
||||||
case "transform-header":
|
case "transform-header":
|
||||||
return actTransformHeader
|
return actTransformHeader
|
||||||
|
case "transform-nth":
|
||||||
|
return actTransformNth
|
||||||
case "transform-prompt":
|
case "transform-prompt":
|
||||||
return actTransformPrompt
|
return actTransformPrompt
|
||||||
case "transform-query":
|
case "transform-query":
|
||||||
return actTransformQuery
|
return actTransformQuery
|
||||||
|
case "transform-search":
|
||||||
|
return actTransformSearch
|
||||||
|
case "search":
|
||||||
|
return actSearch
|
||||||
}
|
}
|
||||||
return actIgnore
|
return actIgnore
|
||||||
}
|
}
|
||||||
@@ -2257,7 +2374,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.Scheme = strings.ToLower(str)
|
if opts.Scheme, opts.Criteria, err = parseScheme(str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "--expect":
|
case "--expect":
|
||||||
str, err := nextString("key names required")
|
str, err := nextString("key names required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2276,6 +2395,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
opts.Phony = false
|
opts.Phony = false
|
||||||
case "--disabled", "--phony":
|
case "--disabled", "--phony":
|
||||||
opts.Phony = true
|
opts.Phony = true
|
||||||
|
case "--no-input":
|
||||||
|
opts.Inputless = true
|
||||||
case "--tiebreak":
|
case "--tiebreak":
|
||||||
str, err := nextString("sort criterion required")
|
str, err := nextString("sort criterion required")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2328,7 +2449,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts.WithNth, err = splitNth(str); err != nil {
|
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "--accept-nth":
|
||||||
|
str, err := nextString("nth expression required")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.AcceptNth, err = nthTransformer(str); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "-s", "--sort":
|
case "-s", "--sort":
|
||||||
@@ -2624,9 +2753,23 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "--min-height":
|
case "--min-height":
|
||||||
if opts.MinHeight, err = nextInt("height required: HEIGHT"); err != nil {
|
expr, err := nextString("minimum height required: HEIGHT[+]")
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
auto := false
|
||||||
|
if strings.HasSuffix(expr, "+") {
|
||||||
|
expr = expr[:len(expr)-1]
|
||||||
|
auto = true
|
||||||
|
}
|
||||||
|
num, err := atoi(expr)
|
||||||
|
if err != nil || num < 0 {
|
||||||
|
return errors.New("minimum height must be a non-negative integer")
|
||||||
|
}
|
||||||
|
if auto {
|
||||||
|
num *= -1
|
||||||
|
}
|
||||||
|
opts.MinHeight = num
|
||||||
case "--no-height":
|
case "--no-height":
|
||||||
opts.Height = heightSpec{}
|
opts.Height = heightSpec{}
|
||||||
case "--no-margin":
|
case "--no-margin":
|
||||||
@@ -2669,6 +2812,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
|
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "--no-header-lines-border":
|
||||||
|
opts.HeaderLinesShape = tui.BorderNone
|
||||||
|
case "--header-lines-border":
|
||||||
|
hasArg, arg := optionalNextString()
|
||||||
|
if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case "--no-header-label":
|
case "--no-header-label":
|
||||||
opts.HeaderLabel.label = ""
|
opts.HeaderLabel.label = ""
|
||||||
case "--header-label":
|
case "--header-label":
|
||||||
@@ -2993,6 +3143,24 @@ func validateOptions(opts *Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func noSeparatorLine(style infoStyle, separator bool) bool {
|
||||||
|
switch style {
|
||||||
|
case infoInline:
|
||||||
|
return true
|
||||||
|
case infoHidden, infoInlineRight:
|
||||||
|
return !separator
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *Options) noSeparatorLine() bool {
|
||||||
|
if opts.Inputless {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
|
||||||
|
return noSeparatorLine(opts.InfoStyle, sep)
|
||||||
|
}
|
||||||
|
|
||||||
// This function can have side-effects and alter some global states.
|
// This function can have side-effects and alter some global states.
|
||||||
// So we run it on fzf.Run and not on ParseOptions.
|
// So we run it on fzf.Run and not on ParseOptions.
|
||||||
func postProcessOptions(opts *Options) error {
|
func postProcessOptions(opts *Options) error {
|
||||||
@@ -3016,6 +3184,19 @@ func postProcessOptions(opts *Options) error {
|
|||||||
opts.HeaderBorderShape = tui.BorderNone
|
opts.HeaderBorderShape = tui.BorderNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.HeaderLinesShape == tui.BorderNone {
|
||||||
|
opts.HeaderLinesShape = tui.BorderPhantom
|
||||||
|
} else if opts.HeaderLinesShape == tui.BorderUndefined {
|
||||||
|
// In reverse-list layout, header lines should be at the top, while
|
||||||
|
// ordinary header should be at the bottom. So let's use a separate
|
||||||
|
// window for the header lines.
|
||||||
|
if opts.Layout == layoutReverseList {
|
||||||
|
opts.HeaderLinesShape = tui.BorderPhantom
|
||||||
|
} else {
|
||||||
|
opts.HeaderLinesShape = tui.BorderNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Pointer == nil {
|
if opts.Pointer == nil {
|
||||||
defaultPointer := "▌"
|
defaultPointer := "▌"
|
||||||
if !opts.Unicode {
|
if !opts.Unicode {
|
||||||
@@ -3116,7 +3297,7 @@ func postProcessOptions(opts *Options) error {
|
|||||||
|
|
||||||
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
|
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
|
||||||
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
|
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
|
||||||
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
|
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.Enter.AsEvent()]
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're not using extended search mode, --nth option becomes irrelevant
|
// If we're not using extended search mode, --nth option becomes irrelevant
|
||||||
@@ -3152,11 +3333,46 @@ func postProcessOptions(opts *Options) error {
|
|||||||
opts.Height = heightSpec{}
|
opts.Height = heightSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets --min-height automatically
|
||||||
|
if opts.Height.size > 0 && opts.Height.percent && opts.MinHeight < 0 {
|
||||||
|
opts.MinHeight = -opts.MinHeight + borderLines(opts.BorderShape) + borderLines(opts.ListBorderShape)
|
||||||
|
if !opts.Inputless {
|
||||||
|
opts.MinHeight += 1 + borderLines(opts.InputBorderShape)
|
||||||
|
if !opts.noSeparatorLine() {
|
||||||
|
opts.MinHeight++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(opts.Header) > 0 {
|
||||||
|
opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header)
|
||||||
|
}
|
||||||
|
if opts.HeaderLines > 0 {
|
||||||
|
borderShape := opts.HeaderBorderShape
|
||||||
|
if opts.HeaderLinesShape.Visible() {
|
||||||
|
borderShape = opts.HeaderLinesShape
|
||||||
|
}
|
||||||
|
opts.MinHeight += borderLines(borderShape) + opts.HeaderLines
|
||||||
|
}
|
||||||
|
if len(opts.Preview.command) > 0 && (opts.Preview.position == posUp || opts.Preview.position == posDown) && opts.Preview.Visible() && opts.Preview.position == posUp {
|
||||||
|
borderShape := opts.Preview.border
|
||||||
|
if opts.Preview.border == tui.BorderLine {
|
||||||
|
borderShape = tui.BorderTop
|
||||||
|
}
|
||||||
|
opts.MinHeight += borderLines(borderShape) + 10
|
||||||
|
}
|
||||||
|
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2], opts.Padding[0], opts.Padding[2]} {
|
||||||
|
if !s.percent {
|
||||||
|
opts.MinHeight += int(s.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := opts.initProfiling(); err != nil {
|
if err := opts.initProfiling(); err != nil {
|
||||||
return errors.New("failed to start pprof profiles: " + err.Error())
|
return errors.New("failed to start pprof profiles: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return processScheme(opts)
|
algo.Init(opts.Scheme)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseShellWords(str string) ([]string, error) {
|
func parseShellWords(str string) ([]string, error) {
|
||||||
@@ -3206,7 +3422,26 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Final validation of merged options
|
// 4. Change default scheme when built-in walker is used
|
||||||
|
if len(opts.Scheme) == 0 {
|
||||||
|
opts.Scheme = "default"
|
||||||
|
if len(opts.Criteria) == 0 {
|
||||||
|
// NOTE: Let's assume $FZF_DEFAULT_COMMAND generates a list of file paths.
|
||||||
|
// But it is possible that it is set to a command that doesn't generate
|
||||||
|
// file paths.
|
||||||
|
//
|
||||||
|
// In that case, you can either
|
||||||
|
// 1. explicitly set --scheme=default,
|
||||||
|
// 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload'
|
||||||
|
// binding, which is the new preferred way.
|
||||||
|
if !opts.hasReloadOrTransformOnStart() && util.IsTty(os.Stdin) {
|
||||||
|
opts.Scheme = "path"
|
||||||
|
}
|
||||||
|
_, opts.Criteria, _ = parseScheme(opts.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Final validation of merged options
|
||||||
if err := validateOptions(opts); err != nil {
|
if err := validateOptions(opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3214,6 +3449,17 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
|||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *Options) hasReloadOrTransformOnStart() bool {
|
||||||
|
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||||
|
for _, action := range actions {
|
||||||
|
if action.t == actReload || action.t == actReloadSync || action.t == actTransform {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *Options) extractReloadOnStart() string {
|
func (opts *Options) extractReloadOnStart() string {
|
||||||
cmd := ""
|
cmd := ""
|
||||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func TestParseKeys(t *testing.T) {
|
|||||||
if len(pairs) != 9 {
|
if len(pairs) != 9 {
|
||||||
t.Error(9)
|
t.Error(9)
|
||||||
}
|
}
|
||||||
check(tui.CtrlM, "Return")
|
check(tui.Enter, "Return")
|
||||||
checkEvent(tui.Key(' '), "space")
|
checkEvent(tui.Key(' '), "space")
|
||||||
check(tui.Tab, "tab")
|
check(tui.Tab, "tab")
|
||||||
check(tui.ShiftTab, "btab")
|
check(tui.ShiftTab, "btab")
|
||||||
@@ -195,7 +195,7 @@ func TestParseKeys(t *testing.T) {
|
|||||||
check(tui.ShiftLeft, "shift-left")
|
check(tui.ShiftLeft, "shift-left")
|
||||||
check(tui.ShiftRight, "shift-right")
|
check(tui.ShiftRight, "shift-right")
|
||||||
check(tui.ShiftTab, "shift-tab")
|
check(tui.ShiftTab, "shift-tab")
|
||||||
check(tui.CtrlM, "Enter")
|
check(tui.Enter, "Enter")
|
||||||
check(tui.Backspace, "bspace")
|
check(tui.Backspace, "bspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ type Pattern struct {
|
|||||||
cacheKey string
|
cacheKey string
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
nth []Range
|
nth []Range
|
||||||
revision int
|
revision revision
|
||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
|
denylist map[int32]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _splitRegex *regexp.Regexp
|
var _splitRegex *regexp.Regexp
|
||||||
@@ -73,7 +74,7 @@ func init() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// BuildPattern builds Pattern object from the given arguments
|
||||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision int, runes []rune) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
revision: revision,
|
revision: revision,
|
||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
|
denylist: denylist,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
ptr.cacheKey = ptr.buildCacheKey()
|
ptr.cacheKey = ptr.buildCacheKey()
|
||||||
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
|
|
||||||
// IsEmpty returns true if the pattern is effectively empty
|
// IsEmpty returns true if the pattern is effectively empty
|
||||||
func (p *Pattern) IsEmpty() bool {
|
func (p *Pattern) IsEmpty() bool {
|
||||||
|
if len(p.denylist) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !p.extended {
|
if !p.extended {
|
||||||
return len(p.text) == 0
|
return len(p.text) == 0
|
||||||
}
|
}
|
||||||
@@ -296,6 +301,8 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
|||||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
|
if len(p.denylist) == 0 {
|
||||||
|
// Huge code duplication for minimizing unnecessary map lookups
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := 0; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||||
@@ -312,6 +319,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if space == nil {
|
||||||
|
for idx := 0; idx < chunk.count; idx++ {
|
||||||
|
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||||
|
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 != nil {
|
||||||
|
matches = append(matches, *match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
// MatchItem returns true if the Item is a match
|
// MatchItem returns true if the Item is a match
|
||||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||||
if p.extended {
|
if p.extended {
|
||||||
@@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
|
|||||||
|
|
||||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||||
ret := Transform(tokens, p.nth)
|
ret := Transform(tokens, p.nth)
|
||||||
|
// Strip the last delimiter to allow suffix match
|
||||||
|
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||||
|
chars := ret[len(ret)-1].text
|
||||||
|
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||||
|
newChars := util.ToChars(stringBytes(stripped))
|
||||||
|
ret[len(ret)-1].text = &newChars
|
||||||
|
}
|
||||||
item.transformed = &transformed{p.revision, ret}
|
item.transformed = &transformed{p.revision, ret}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||||
withPos, cacheable, nth, delimiter, 0, runes)
|
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
|||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
path += sep
|
||||||
}
|
}
|
||||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
}
|
}
|
||||||
case byLength:
|
case byLength:
|
||||||
val = item.TrimLength()
|
val = item.TrimLength()
|
||||||
|
case byPathname:
|
||||||
|
if validOffsetFound {
|
||||||
|
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
||||||
|
lastDelim := -1
|
||||||
|
s := item.text.ToString()
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if s[i] == '/' || s[i] == '\\' {
|
||||||
|
lastDelim = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastDelim <= minBegin {
|
||||||
|
val = util.AsUint16(minBegin - lastDelim)
|
||||||
|
}
|
||||||
|
}
|
||||||
case byBegin, byEnd:
|
case byBegin, byEnd:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
whitePrefixLen := 0
|
whitePrefixLen := 0
|
||||||
|
|||||||
780
src/terminal.go
780
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -485,6 +485,11 @@ func TestParsePlaceholder(t *testing.T) {
|
|||||||
// query flag is not removed after parsing, so it gets doubled
|
// query flag is not removed after parsing, so it gets doubled
|
||||||
// while the double q is invalid, it is useful here for testing purposes
|
// while the double q is invalid, it is useful here for testing purposes
|
||||||
`{q}`: `{qq}`,
|
`{q}`: `{qq}`,
|
||||||
|
`{q:1}`: `{qq:1}`,
|
||||||
|
`{q:2..}`: `{qq:2..}`,
|
||||||
|
`{q:..}`: `{qq:..}`,
|
||||||
|
`{q:2..-1}`: `{qq:2..-1}`,
|
||||||
|
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
|
||||||
|
|
||||||
// IV. escaping placeholder
|
// IV. escaping placeholder
|
||||||
`\{}`: `{}`,
|
`\{}`: `{}`,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,18 @@ func (r Range) IsFull() bool {
|
|||||||
return r.begin == rangeEllipsis && r.end == rangeEllipsis
|
return r.begin == rangeEllipsis && r.end == rangeEllipsis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compareRanges(r1 []Range, r2 []Range) bool {
|
||||||
|
if len(r1) != len(r2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for idx := range r1 {
|
||||||
|
if r1[idx] != r2[idx] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func RangesToString(ranges []Range) string {
|
func RangesToString(ranges []Range) string {
|
||||||
strs := []string{}
|
strs := []string{}
|
||||||
for _, r := range ranges {
|
for _, r := range ranges {
|
||||||
@@ -65,6 +78,11 @@ type Delimiter struct {
|
|||||||
str *string
|
str *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||||
|
func (d Delimiter) IsAwk() bool {
|
||||||
|
return d.regex == nil && d.str == nil
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the string representation of a Delimiter.
|
// String returns the string representation of a Delimiter.
|
||||||
func (d Delimiter) String() string {
|
func (d Delimiter) String() string {
|
||||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||||
@@ -199,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
|||||||
return withPrefixLengths(tokens, 0)
|
return withPrefixLengths(tokens, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinTokens(tokens []Token) string {
|
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||||
|
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||||
|
if delimiter.str != 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]
|
||||||
|
if lastLoc[1] == len(str) {
|
||||||
|
str = str[:lastLoc[0]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinTokens concatenates the tokens into a single string
|
||||||
|
func JoinTokens(tokens []Token) string {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
output.WriteString(token.text.ToString())
|
output.WriteString(token.text.ToString())
|
||||||
@@ -217,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
if r.begin == r.end {
|
if r.begin == r.end {
|
||||||
idx := r.begin
|
idx := r.begin
|
||||||
if idx == rangeEllipsis {
|
if idx == rangeEllipsis {
|
||||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||||
parts = append(parts, &chars)
|
parts = append(parts, &chars)
|
||||||
} else {
|
} else {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ranges, _ := splitNth("1,2,3")
|
ranges, _ := splitNth("1,2,3")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if joinTokens(tx) != "abc: def: ghi: " {
|
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||||
t.Errorf("%s", tx)
|
t.Errorf("%s", tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ranges, _ := splitNth("1..2,3,2..,1")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||||
len(tx) != 4 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||||
@@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ranges, _ := splitNth("1..2,3,2..,1")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||||
len(tx) != 4 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ func (r *FullscreenRenderer) PassThrough(string) {}
|
|||||||
func (r *FullscreenRenderer) Clear() {}
|
func (r *FullscreenRenderer) Clear() {}
|
||||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||||
|
func (r *FullscreenRenderer) Bell() {}
|
||||||
|
func (r *FullscreenRenderer) HideCursor() {}
|
||||||
|
func (r *FullscreenRenderer) ShowCursor() {}
|
||||||
func (r *FullscreenRenderer) Refresh() {}
|
func (r *FullscreenRenderer) Refresh() {}
|
||||||
func (r *FullscreenRenderer) Close() {}
|
func (r *FullscreenRenderer) Close() {}
|
||||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func _() {
|
|||||||
_ = x[CtrlJ-10]
|
_ = x[CtrlJ-10]
|
||||||
_ = x[CtrlK-11]
|
_ = x[CtrlK-11]
|
||||||
_ = x[CtrlL-12]
|
_ = x[CtrlL-12]
|
||||||
_ = x[CtrlM-13]
|
_ = x[Enter-13]
|
||||||
_ = x[CtrlN-14]
|
_ = x[CtrlN-14]
|
||||||
_ = x[CtrlO-15]
|
_ = x[CtrlO-15]
|
||||||
_ = x[CtrlP-16]
|
_ = x[CtrlP-16]
|
||||||
@@ -110,7 +110,7 @@ func _() {
|
|||||||
_ = x[ClickHeader-99]
|
_ = x[ClickHeader-99]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||||
|
|
||||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ const (
|
|||||||
|
|
||||||
const consoleDevice string = "/dev/tty"
|
const consoleDevice string = "/dev/tty"
|
||||||
|
|
||||||
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
|
var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
|
||||||
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||||
|
|
||||||
|
func (r *LightRenderer) Bell() {
|
||||||
|
r.flushRaw("\a")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) PassThrough(str string) {
|
func (r *LightRenderer) PassThrough(str string) {
|
||||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||||
}
|
}
|
||||||
@@ -40,8 +44,9 @@ func (r *LightRenderer) stderr(str string) {
|
|||||||
r.stderrInternal(str, true, "")
|
r.stderrInternal(str, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const CR string = "\x1b[2m␍"
|
const DIM string = "\x1b[2m"
|
||||||
const LF string = "\x1b[2m␊"
|
const CR string = DIM + "␍"
|
||||||
|
const LF string = DIM + "␊"
|
||||||
|
|
||||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
bytes := []byte(str)
|
bytes := []byte(str)
|
||||||
@@ -73,7 +78,13 @@ func (r *LightRenderer) csi(code string) string {
|
|||||||
|
|
||||||
func (r *LightRenderer) flush() {
|
func (r *LightRenderer) flush() {
|
||||||
if r.queued.Len() > 0 {
|
if r.queued.Len() > 0 {
|
||||||
r.flushRaw("\x1b[?7l\x1b[?25l" + r.queued.String() + "\x1b[?25h\x1b[?7h")
|
raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
|
||||||
|
if r.showCursor {
|
||||||
|
raw += "\x1b[?25h\x1b[?7h"
|
||||||
|
} else {
|
||||||
|
raw += "\x1b[?7h"
|
||||||
|
}
|
||||||
|
r.flushRaw(raw)
|
||||||
r.queued.Reset()
|
r.queued.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +117,7 @@ type LightRenderer struct {
|
|||||||
y int
|
y int
|
||||||
x int
|
x int
|
||||||
maxHeightFunc func(int) int
|
maxHeightFunc func(int) int
|
||||||
|
showCursor bool
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
ttyinChannel chan byte
|
ttyinChannel chan byte
|
||||||
@@ -129,6 +141,8 @@ type LightWindow struct {
|
|||||||
tabstop int
|
tabstop int
|
||||||
fg Color
|
fg Color
|
||||||
bg Color
|
bg Color
|
||||||
|
wrapSign string
|
||||||
|
wrapSignWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||||
@@ -148,7 +162,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
|
|||||||
tabstop: tabstop,
|
tabstop: tabstop,
|
||||||
fullscreen: fullscreen,
|
fullscreen: fullscreen,
|
||||||
upOneLine: false,
|
upOneLine: false,
|
||||||
maxHeightFunc: maxHeightFunc}
|
maxHeightFunc: maxHeightFunc,
|
||||||
|
showCursor: true}
|
||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,15 +637,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
|||||||
|
|
||||||
// middle := t & 0b1
|
// middle := t & 0b1
|
||||||
left := t&0b11 == 0
|
left := t&0b11 == 0
|
||||||
|
ctrl := t&0b10000 > 0
|
||||||
// shift := t & 0b100
|
alt := t&0b01000 > 0
|
||||||
// ctrl := t & 0b1000
|
shift := t&0b00100 > 0
|
||||||
mod := t&0b1100 > 0
|
drag := t&0b100000 > 0 // 32
|
||||||
|
|
||||||
drag := t&0b100000 > 0
|
|
||||||
|
|
||||||
if scroll != 0 {
|
if scroll != 0 {
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, ctrl, alt, shift}}
|
||||||
}
|
}
|
||||||
|
|
||||||
double := false
|
double := false
|
||||||
@@ -654,7 +667,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) smcup() {
|
func (r *LightRenderer) smcup() {
|
||||||
@@ -757,6 +770,9 @@ func (r *LightRenderer) Close() {
|
|||||||
} else if !r.fullscreen {
|
} else if !r.fullscreen {
|
||||||
r.csi("u")
|
r.csi("u")
|
||||||
}
|
}
|
||||||
|
if !r.showCursor {
|
||||||
|
r.csi("?25h")
|
||||||
|
}
|
||||||
r.disableMouse()
|
r.disableMouse()
|
||||||
r.flush()
|
r.flush()
|
||||||
r.closePlatform()
|
r.closePlatform()
|
||||||
@@ -1092,11 +1108,12 @@ type wrappedLine struct {
|
|||||||
displayWidth int
|
displayWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
||||||
lines := []wrappedLine{}
|
lines := []wrappedLine{}
|
||||||
width := 0
|
width := 0
|
||||||
line := ""
|
line := ""
|
||||||
gr := uniseg.NewGraphemes(input)
|
gr := uniseg.NewGraphemes(input)
|
||||||
|
max := initialMax
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
str := string(rs)
|
str := string(rs)
|
||||||
@@ -1118,6 +1135,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
line = str
|
line = str
|
||||||
prefixLength = 0
|
prefixLength = 0
|
||||||
width = w
|
width = w
|
||||||
|
max = initialMax - wrapSignWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, wrappedLine{string(line), width})
|
lines = append(lines, wrappedLine{string(line), width})
|
||||||
@@ -1127,7 +1145,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
for i, line := range allLines {
|
||||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||||
for j, wl := range lines {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false, resetCode)
|
w.stderrInternal(wl.text, false, resetCode)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.displayWidth
|
||||||
@@ -1140,6 +1158,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
|||||||
w.MoveAndClear(w.posy, w.posx)
|
w.MoveAndClear(w.posy, w.posx)
|
||||||
w.Move(w.posy+1, 0)
|
w.Move(w.posy+1, 0)
|
||||||
w.renderer.stderr(resetCode)
|
w.renderer.stderr(resetCode)
|
||||||
|
if len(lines) > 1 {
|
||||||
|
sign := w.wrapSign
|
||||||
|
width := w.wrapSignWidth
|
||||||
|
if width > w.width {
|
||||||
|
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
|
||||||
|
sign = string(runes)
|
||||||
|
width = truncatedWidth
|
||||||
|
}
|
||||||
|
w.stderrInternal(DIM+sign, false, resetCode)
|
||||||
|
w.renderer.stderr(resetCode)
|
||||||
|
w.Move(w.posy, width)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1212,3 +1242,18 @@ func (w *LightWindow) Erase() {
|
|||||||
func (w *LightWindow) EraseMaybe() bool {
|
func (w *LightWindow) EraseMaybe() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) SetWrapSign(sign string, width int) {
|
||||||
|
w.wrapSign = sign
|
||||||
|
w.wrapSignWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) HideCursor() {
|
||||||
|
r.showCursor = false
|
||||||
|
r.csi("?25l")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) ShowCursor() {
|
||||||
|
r.showCursor = true
|
||||||
|
r.csi("?25h")
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ type TcellWindow struct {
|
|||||||
borderStyle BorderStyle
|
borderStyle BorderStyle
|
||||||
uri *string
|
uri *string
|
||||||
params *string
|
params *string
|
||||||
|
showCursor bool
|
||||||
|
wrapSign string
|
||||||
|
wrapSignWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Top() int {
|
func (w *TcellWindow) Top() int {
|
||||||
@@ -72,7 +75,9 @@ func (w *TcellWindow) Height() int {
|
|||||||
|
|
||||||
func (w *TcellWindow) Refresh() {
|
func (w *TcellWindow) Refresh() {
|
||||||
if w.moveCursor {
|
if w.moveCursor {
|
||||||
|
if w.showCursor {
|
||||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||||
|
}
|
||||||
w.moveCursor = false
|
w.moveCursor = false
|
||||||
}
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
@@ -100,6 +105,18 @@ const (
|
|||||||
BoldForce = Attr(1 << 10)
|
BoldForce = Attr(1 << 10)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) Bell() {
|
||||||
|
_screen.Beep()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) HideCursor() {
|
||||||
|
r.showCursor = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) ShowCursor() {
|
||||||
|
r.showCursor = true
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||||
// No-op
|
// No-op
|
||||||
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
||||||
@@ -164,6 +181,9 @@ func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
|
if !r.showCursor {
|
||||||
|
s.HideCursor()
|
||||||
|
}
|
||||||
_screen = s
|
_screen = s
|
||||||
}
|
}
|
||||||
return _screen, nil
|
return _screen, nil
|
||||||
@@ -262,7 +282,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||||
// dragging has same structure, it only repeats the middle (main) event appropriately
|
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||||
x, y := ev.Position()
|
x, y := ev.Position()
|
||||||
mod := ev.Modifiers() != 0
|
|
||||||
|
mod := ev.Modifiers()
|
||||||
|
ctrl := (mod & tcell.ModCtrl) > 0
|
||||||
|
alt := (mod & tcell.ModAlt) > 0
|
||||||
|
shift := (mod & tcell.ModShift) > 0
|
||||||
|
|
||||||
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||||
@@ -271,9 +295,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case button&tcell.WheelDown != 0:
|
case button&tcell.WheelDown != 0:
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
|
||||||
case button&tcell.WheelUp != 0:
|
case button&tcell.WheelUp != 0:
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
|
||||||
case button&tcell.Button1 != 0:
|
case button&tcell.Button1 != 0:
|
||||||
double := false
|
double := false
|
||||||
if !drag {
|
if !drag {
|
||||||
@@ -296,9 +320,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fire single or double click event
|
// fire single or double click event
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
|
||||||
case button&tcell.Button2 != 0:
|
case button&tcell.Button2 != 0:
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
|
||||||
default:
|
default:
|
||||||
// double and single taps on Windows don't quite work due to
|
// double and single taps on Windows don't quite work due to
|
||||||
// the console acting on the events and not allowing us
|
// the console acting on the events and not allowing us
|
||||||
@@ -307,7 +331,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
down := left || button&tcell.Button3 != 0
|
down := left || button&tcell.Button3 != 0
|
||||||
double := false
|
double := false
|
||||||
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
// No need to report mouse movement events when no button is pressed
|
||||||
|
if drag {
|
||||||
|
return Event{Invalid, 0, nil}
|
||||||
|
}
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process keyboard:
|
// process keyboard:
|
||||||
@@ -534,7 +562,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||||
if clear {
|
if clear {
|
||||||
_screen.Fini()
|
r.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +574,7 @@ func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) Close() {
|
func (r *FullscreenRenderer) Close() {
|
||||||
_screen.Fini()
|
_screen.Fini()
|
||||||
|
_screen = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||||
@@ -578,7 +607,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
normal: normal,
|
normal: normal,
|
||||||
borderStyle: borderStyle}
|
borderStyle: borderStyle,
|
||||||
|
showCursor: r.showCursor}
|
||||||
w.Erase()
|
w.Erase()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -601,6 +631,11 @@ func (w *TcellWindow) EraseMaybe() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||||
|
w.wrapSign = sign
|
||||||
|
w.wrapSignWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) EncloseX(x int) bool {
|
func (w *TcellWindow) EncloseX(x int) bool {
|
||||||
return x >= w.left && x < (w.left+w.width)
|
return x >= w.left && x < (w.left+w.width)
|
||||||
}
|
}
|
||||||
@@ -729,11 +764,26 @@ Loop:
|
|||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
xPos := w.left + w.lastX + lx
|
xPos := w.left + w.lastX + lx
|
||||||
if xPos >= (w.left + w.width) {
|
if xPos >= w.left+w.width {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
|
if w.lastY >= w.height {
|
||||||
|
return FillSuspend
|
||||||
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
xPos = w.left
|
xPos = w.left
|
||||||
|
sign := w.wrapSign
|
||||||
|
if w.wrapSignWidth > w.width {
|
||||||
|
runes, _ := util.Truncate(sign, w.width)
|
||||||
|
sign = string(runes)
|
||||||
|
}
|
||||||
|
wgr := uniseg.NewGraphemes(sign)
|
||||||
|
for wgr.Next() {
|
||||||
|
rs := wgr.Runes()
|
||||||
|
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||||
|
lx += uniseg.StringWidth(string(rs))
|
||||||
|
}
|
||||||
|
xPos = w.left + lx
|
||||||
}
|
}
|
||||||
|
|
||||||
yPos := w.top + w.lastY
|
yPos := w.top + w.lastY
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
|
|||||||
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||||
// KeyEnter is alias for KeyCR
|
// KeyEnter is alias for KeyCR
|
||||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||||
// Ctrl+Alt keys
|
// Ctrl+Alt keys
|
||||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
@@ -233,7 +233,7 @@ Quick reference
|
|||||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
|
||||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const (
|
|||||||
CtrlJ
|
CtrlJ
|
||||||
CtrlK
|
CtrlK
|
||||||
CtrlL
|
CtrlL
|
||||||
CtrlM
|
Enter
|
||||||
CtrlN
|
CtrlN
|
||||||
CtrlO
|
CtrlO
|
||||||
CtrlP
|
CtrlP
|
||||||
@@ -150,12 +150,19 @@ func (e Event) Comparable() Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e Event) KeyName() string {
|
func (e Event) KeyName() string {
|
||||||
|
if me := e.MouseEvent; me != nil {
|
||||||
|
return me.Name()
|
||||||
|
}
|
||||||
|
|
||||||
if e.Type >= Invalid {
|
if e.Type >= Invalid {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case Rune:
|
case Rune:
|
||||||
|
if e.Char == ' ' {
|
||||||
|
return "space"
|
||||||
|
}
|
||||||
return string(e.Char)
|
return string(e.Char)
|
||||||
case Alt:
|
case Alt:
|
||||||
return "alt-" + string(e.Char)
|
return "alt-" + string(e.Char)
|
||||||
@@ -367,7 +374,37 @@ type MouseEvent struct {
|
|||||||
Left bool
|
Left bool
|
||||||
Down bool
|
Down bool
|
||||||
Double bool
|
Double bool
|
||||||
Mod bool
|
Ctrl bool
|
||||||
|
Alt bool
|
||||||
|
Shift bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) Mod() bool {
|
||||||
|
return e.Ctrl || e.Alt || e.Shift
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MouseEvent) Name() string {
|
||||||
|
name := ""
|
||||||
|
if e.Down {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Ctrl {
|
||||||
|
name += "ctrl-"
|
||||||
|
}
|
||||||
|
if e.Alt {
|
||||||
|
name += "alt-"
|
||||||
|
}
|
||||||
|
if e.Shift {
|
||||||
|
name += "shift-"
|
||||||
|
}
|
||||||
|
if e.Double {
|
||||||
|
name += "double-"
|
||||||
|
}
|
||||||
|
if !e.Left {
|
||||||
|
name += "right-"
|
||||||
|
}
|
||||||
|
return name + "click"
|
||||||
}
|
}
|
||||||
|
|
||||||
type BorderShape int
|
type BorderShape int
|
||||||
@@ -376,6 +413,7 @@ const (
|
|||||||
BorderUndefined BorderShape = iota
|
BorderUndefined BorderShape = iota
|
||||||
BorderLine
|
BorderLine
|
||||||
BorderNone
|
BorderNone
|
||||||
|
BorderPhantom
|
||||||
BorderRounded
|
BorderRounded
|
||||||
BorderSharp
|
BorderSharp
|
||||||
BorderBold
|
BorderBold
|
||||||
@@ -392,7 +430,7 @@ const (
|
|||||||
|
|
||||||
func (s BorderShape) HasLeft() bool {
|
func (s BorderShape) HasLeft() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -400,7 +438,7 @@ func (s BorderShape) HasLeft() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasRight() bool {
|
func (s BorderShape) HasRight() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -408,7 +446,7 @@ func (s BorderShape) HasRight() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasTop() bool {
|
func (s BorderShape) HasTop() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -416,7 +454,7 @@ func (s BorderShape) HasTop() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasBottom() bool {
|
func (s BorderShape) HasBottom() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -441,7 +479,7 @@ type BorderStyle struct {
|
|||||||
type BorderCharacter int
|
type BorderCharacter int
|
||||||
|
|
||||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||||
if shape == BorderNone {
|
if shape == BorderNone || shape == BorderPhantom {
|
||||||
return BorderStyle{
|
return BorderStyle{
|
||||||
shape: shape,
|
shape: shape,
|
||||||
top: ' ',
|
top: ' ',
|
||||||
@@ -579,6 +617,9 @@ type Renderer interface {
|
|||||||
PassThrough(string)
|
PassThrough(string)
|
||||||
NeedScrollbarRedraw() bool
|
NeedScrollbarRedraw() bool
|
||||||
ShouldEmitResizeEvent() bool
|
ShouldEmitResizeEvent() bool
|
||||||
|
Bell()
|
||||||
|
HideCursor()
|
||||||
|
ShowCursor()
|
||||||
|
|
||||||
GetChar() Event
|
GetChar() Event
|
||||||
|
|
||||||
@@ -618,6 +659,8 @@ type Window interface {
|
|||||||
LinkEnd()
|
LinkEnd()
|
||||||
Erase()
|
Erase()
|
||||||
EraseMaybe() bool
|
EraseMaybe() bool
|
||||||
|
|
||||||
|
SetWrapSign(string, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullscreenRenderer struct {
|
type FullscreenRenderer struct {
|
||||||
@@ -626,6 +669,7 @@ type FullscreenRenderer struct {
|
|||||||
forceBlack bool
|
forceBlack bool
|
||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
|
showCursor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||||
@@ -634,7 +678,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
|||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
prevDownTime: time.Unix(0, 0),
|
prevDownTime: time.Unix(0, 0),
|
||||||
clicks: [][2]int{}}
|
clicks: [][2]int{},
|
||||||
|
showCursor: true}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,9 +184,25 @@ func (chars *Chars) TrailingWhitespaces() int {
|
|||||||
return whitespaces
|
return whitespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||||
whitespaces := chars.TrailingWhitespaces()
|
lastIdx := len(chars.slice)
|
||||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
firstIdx := lastIdx - len(runes)
|
||||||
|
if firstIdx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := firstIdx; i < lastIdx; i++ {
|
||||||
|
char := chars.Get(i)
|
||||||
|
if char != runes[i-firstIdx] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chars.slice = chars.slice[0:firstIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) SliceRight(last int) {
|
||||||
|
chars.slice = chars.slice[:last]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) ToString() string {
|
func (chars *Chars) ToString() string {
|
||||||
|
|||||||
240
test/lib/common.rb
Normal file
240
test/lib/common.rb
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'minitest/autorun'
|
||||||
|
require 'fileutils'
|
||||||
|
require 'English'
|
||||||
|
require 'shellwords'
|
||||||
|
require 'erb'
|
||||||
|
require 'tempfile'
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
||||||
|
UNSETS = %w[
|
||||||
|
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
||||||
|
FZF_TMUX FZF_TMUX_OPTS
|
||||||
|
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||||
|
FZF_ALT_C_COMMAND
|
||||||
|
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||||
|
FZF_API_KEY
|
||||||
|
].freeze
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
FILE = File.expand_path(__FILE__)
|
||||||
|
BASE = File.expand_path('../..', __dir__)
|
||||||
|
Dir.chdir(BASE)
|
||||||
|
FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf".freeze
|
||||||
|
|
||||||
|
def wait(timeout = DEFAULT_TIMEOUT)
|
||||||
|
since = Time.now
|
||||||
|
begin
|
||||||
|
yield or raise Minitest::Assertion, 'Assertion failure'
|
||||||
|
rescue Minitest::Assertion
|
||||||
|
raise if Time.now - since > timeout
|
||||||
|
|
||||||
|
sleep(0.05)
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Shell
|
||||||
|
class << self
|
||||||
|
def bash
|
||||||
|
@bash ||=
|
||||||
|
begin
|
||||||
|
bashrc = '/tmp/fzf.bash'
|
||||||
|
File.open(bashrc, 'w') do |f|
|
||||||
|
f.puts ERB.new(TEMPLATE).result(binding)
|
||||||
|
end
|
||||||
|
|
||||||
|
"bash --rcfile #{bashrc}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def zsh
|
||||||
|
@zsh ||=
|
||||||
|
begin
|
||||||
|
zdotdir = '/tmp/fzf-zsh'
|
||||||
|
FileUtils.rm_rf(zdotdir)
|
||||||
|
FileUtils.mkdir_p(zdotdir)
|
||||||
|
File.open("#{zdotdir}/.zshrc", 'w') do |f|
|
||||||
|
f.puts ERB.new(TEMPLATE).result(binding)
|
||||||
|
end
|
||||||
|
"ZDOTDIR=#{zdotdir} zsh"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fish
|
||||||
|
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Tmux
|
||||||
|
attr_reader :win
|
||||||
|
|
||||||
|
def initialize(shell = :bash)
|
||||||
|
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
|
||||||
|
go(%W[set-window-option -t #{@win} pane-base-index 0])
|
||||||
|
return unless shell == :fish
|
||||||
|
|
||||||
|
send_keys 'function fish_prompt; end; clear', :Enter
|
||||||
|
self.until(&:empty?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def kill
|
||||||
|
go(%W[kill-window -t #{win}])
|
||||||
|
end
|
||||||
|
|
||||||
|
def focus
|
||||||
|
go(%W[select-window -t #{win}])
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_keys(*args)
|
||||||
|
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
||||||
|
end
|
||||||
|
|
||||||
|
def paste(str)
|
||||||
|
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
||||||
|
end
|
||||||
|
|
||||||
|
def capture
|
||||||
|
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||||
|
lines = nil
|
||||||
|
begin
|
||||||
|
wait(timeout) do
|
||||||
|
lines = capture
|
||||||
|
class << lines
|
||||||
|
def counts
|
||||||
|
lazy
|
||||||
|
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
|
||||||
|
.reject(&:empty?)
|
||||||
|
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_count
|
||||||
|
counts[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def item_count
|
||||||
|
counts[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_count
|
||||||
|
counts[2]
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_include?(val)
|
||||||
|
method = val.is_a?(Regexp) ? :match : :include?
|
||||||
|
find { |line| line.send(method, val) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
yield(lines).tap do |ok|
|
||||||
|
send_keys 'C-l' if refresh && !ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Minitest::Assertion
|
||||||
|
puts $ERROR_INFO.backtrace
|
||||||
|
puts '>' * 80
|
||||||
|
puts lines
|
||||||
|
puts '<' * 80
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
lines
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare
|
||||||
|
tries = 0
|
||||||
|
begin
|
||||||
|
self.until(true) do |lines|
|
||||||
|
message = "Prepare[#{tries}]"
|
||||||
|
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||||
|
lines[-1] == message
|
||||||
|
end
|
||||||
|
rescue Minitest::Assertion
|
||||||
|
(tries += 1) < 5 ? retry : raise
|
||||||
|
end
|
||||||
|
send_keys 'C-u', 'C-l'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def go(args)
|
||||||
|
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TestBase < Minitest::Test
|
||||||
|
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
|
||||||
|
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
|
||||||
|
|
||||||
|
def writelines(lines)
|
||||||
|
File.write(TEMPNAME, lines.join("\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def tempname
|
||||||
|
TEMPNAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def fzf_output
|
||||||
|
@thread.join.value.chomp.tap { @thread = nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
def fzf_output_lines
|
||||||
|
fzf_output.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup
|
||||||
|
File.mkfifo(FIFONAME)
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
FileUtils.rm_f([TEMPNAME, FIFONAME])
|
||||||
|
end
|
||||||
|
|
||||||
|
alias assert_equal_org assert_equal
|
||||||
|
def assert_equal(expected, actual)
|
||||||
|
# Ignore info separator
|
||||||
|
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
|
||||||
|
assert_equal_org(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run fzf with its output piped to a fifo
|
||||||
|
def fzf(*opts)
|
||||||
|
raise 'fzf_output not taken' if @thread
|
||||||
|
|
||||||
|
@thread = Thread.new { File.read(FIFONAME) }
|
||||||
|
fzf!(*opts) + " > #{FIFONAME.shellescape}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def fzf!(*opts)
|
||||||
|
opts = opts.filter_map do |o|
|
||||||
|
case o
|
||||||
|
when Symbol
|
||||||
|
o = o.to_s
|
||||||
|
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
|
||||||
|
when String, Numeric
|
||||||
|
o.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"#{FZF} #{opts.join(' ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TestInteractive < TestBase
|
||||||
|
attr_reader :tmux
|
||||||
|
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
@tmux = Tmux.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
super
|
||||||
|
@tmux.kill
|
||||||
|
end
|
||||||
|
end
|
||||||
59
test/lib/common.sh
Normal file
59
test/lib/common.sh
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
set -u
|
||||||
|
PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
|
||||||
|
unset <%= UNSETS.join(' ') %>
|
||||||
|
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
|
||||||
|
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
|
||||||
|
|
||||||
|
export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
|
||||||
|
|
||||||
|
# Setup fzf
|
||||||
|
# ---------
|
||||||
|
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
|
||||||
|
export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-completion
|
||||||
|
# ---------------
|
||||||
|
[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
|
||||||
|
|
||||||
|
# Key bindings
|
||||||
|
# ------------
|
||||||
|
source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
|
||||||
|
|
||||||
|
# Old API
|
||||||
|
_fzf_complete_f() {
|
||||||
|
_fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <(
|
||||||
|
echo foo
|
||||||
|
echo bar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# New API
|
||||||
|
_fzf_complete_g() {
|
||||||
|
_fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <(
|
||||||
|
echo foo
|
||||||
|
echo bar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_complete_f_post() {
|
||||||
|
awk '{print "f" $0 $0}'
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_complete_g_post() {
|
||||||
|
awk '{print "g" $0 $0}'
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "${BASH-}" ] && complete -F _fzf_complete_f -o default -o bashdefault f
|
||||||
|
[ -n "${BASH-}" ] && complete -F _fzf_complete_g -o default -o bashdefault g
|
||||||
|
|
||||||
|
_comprun() {
|
||||||
|
local command=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
f) fzf "$@" --preview 'echo preview-f-{}' ;;
|
||||||
|
g) fzf "$@" --preview 'echo preview-g-{}' ;;
|
||||||
|
*) fzf "$@" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
5
test/runner.rb
Normal file
5
test/runner.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Dir[File.join(__dir__, 'test_*.rb')].each { require it }
|
||||||
|
|
||||||
|
require 'minitest/autorun'
|
||||||
1811
test/test_core.rb
Normal file
1811
test/test_core.rb
Normal file
File diff suppressed because it is too large
Load Diff
417
test/test_exec.rb
Normal file
417
test/test_exec.rb
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Process execution: execute, become, reload
|
||||||
|
class TestExec < TestInteractive
|
||||||
|
def test_execute
|
||||||
|
output = '/tmp/fzf-test-execute'
|
||||||
|
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
|
||||||
|
writelines(%w[foo'bar foo"bar foo$bar])
|
||||||
|
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
|
||||||
|
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
tmux.send_keys :Escape, :b
|
||||||
|
ready.call('alt-b')
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
tmux.send_keys :Escape, :b
|
||||||
|
ready.call('alt-b')
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.send_keys :C
|
||||||
|
ready.call('C')
|
||||||
|
|
||||||
|
tmux.send_keys 'barfoo'
|
||||||
|
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
|
||||||
|
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
tmux.send_keys :Escape, :b
|
||||||
|
ready.call('alt-b')
|
||||||
|
|
||||||
|
wait do
|
||||||
|
assert_path_exists output
|
||||||
|
assert_equal %w[
|
||||||
|
/foo'bar/ /foo'barfoo'bar/
|
||||||
|
/foo"bar/ /foo"barfoo"bar/
|
||||||
|
/foo$barfoo$barfoo$bar/
|
||||||
|
], File.readlines(output, chomp: true)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_execute_multi
|
||||||
|
output = '/tmp/fzf-test-execute-multi'
|
||||||
|
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
|
||||||
|
writelines(%w[foo'bar foo"bar foo$bar foobar])
|
||||||
|
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||||
|
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||||
|
|
||||||
|
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
tmux.send_keys :Escape, :b
|
||||||
|
ready.call('alt-b')
|
||||||
|
|
||||||
|
tmux.send_keys :BTab, :BTab, :BTab
|
||||||
|
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
tmux.send_keys :Escape, :b
|
||||||
|
ready.call('alt-b')
|
||||||
|
|
||||||
|
tmux.send_keys :Tab, :Tab
|
||||||
|
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||||
|
tmux.send_keys :Escape, :a
|
||||||
|
ready.call('alt-a')
|
||||||
|
wait do
|
||||||
|
assert_path_exists output
|
||||||
|
assert_equal [
|
||||||
|
%(foo'bar/foo'bar),
|
||||||
|
%(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar),
|
||||||
|
%(foo'bar foo"bar foobar/foo'bar foo"bar foobar)
|
||||||
|
], File.readlines(output, chomp: true)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_execute_plus_flag
|
||||||
|
output = tempname + '.tmp'
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
writelines(['foo bar', '123 456'])
|
||||||
|
|
||||||
|
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||||
|
|
||||||
|
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||||
|
tmux.send_keys 'xy'
|
||||||
|
tmux.until { |lines| assert_equal ' 0/2 (0)', lines[-2] }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.send_keys 'xy'
|
||||||
|
tmux.until { |lines| assert_equal ' 0/2 (1)', lines[-2] }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal ' 2/2 (1)', lines[-2] }
|
||||||
|
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.send_keys 'xy'
|
||||||
|
tmux.until { |lines| assert_equal ' 0/2 (2)', lines[-2] }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] }
|
||||||
|
|
||||||
|
wait do
|
||||||
|
assert_path_exists output
|
||||||
|
assert_equal [
|
||||||
|
%(foo bar/foo bar/bar/bar),
|
||||||
|
%(123 456/foo bar/456/bar),
|
||||||
|
%(123 456 foo bar/foo bar/456 bar/bar)
|
||||||
|
], File.readlines(output, chomp: true)
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_execute_shell
|
||||||
|
# Custom script to use as $SHELL
|
||||||
|
output = tempname + '.out'
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||||
|
system("chmod +x #{tempname}")
|
||||||
|
|
||||||
|
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||||
|
wait do
|
||||||
|
assert_path_exists output
|
||||||
|
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_interrupt_execute
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys 'C-l'
|
||||||
|
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys 99
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_default_command_on_abort
|
||||||
|
writelines(['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{tempname}")
|
||||||
|
|
||||||
|
tmux.send_keys FZF.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
wait { refute system("pgrep -f #{tempname}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{tempname}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_default_command_on_accept
|
||||||
|
writelines(['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{tempname}")
|
||||||
|
|
||||||
|
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal 'Started', fzf_output
|
||||||
|
wait { refute system("pgrep -f #{tempname}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{tempname}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_reload_command_on_abort
|
||||||
|
writelines(['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{tempname}")
|
||||||
|
|
||||||
|
tmux.send_keys "seq 1 3 | #{FZF} --bind 'ctrl-r:reload(#{tempname})'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
wait { refute system("pgrep -f #{tempname}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{tempname}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_kill_reload_command_on_accept
|
||||||
|
writelines(['#!/usr/bin/env bash',
|
||||||
|
"echo 'Started'",
|
||||||
|
'while :; do sleep 1; done'])
|
||||||
|
system("chmod +x #{tempname}")
|
||||||
|
|
||||||
|
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{tempname})'")}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal 'Started', fzf_output
|
||||||
|
wait { refute system("pgrep -f #{tempname}") }
|
||||||
|
ensure
|
||||||
|
system("pkill -9 -f #{tempname}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload
|
||||||
|
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 998, lines.match_count }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 98, lines.item_count
|
||||||
|
assert_equal 98, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys 'b'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 198, lines.item_count
|
||||||
|
assert_equal 198, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal ' 198/198 (1/2)', lines[-2] }
|
||||||
|
tmux.send_keys '555'
|
||||||
|
tmux.until { |lines| assert_equal ' 1/553 (0/2)', lines[-2] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_even_when_theres_no_match
|
||||||
|
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_should_terminate_standard_input_stream
|
||||||
|
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_clear_list_when_header_lines_changed_due_to_reload
|
||||||
|
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines, ' 9' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| refute_includes lines, ' 9' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_item_index_reset_on_reload
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
assert_includes lines[1], '[[0]]'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_should_update_preview
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '1' }
|
||||||
|
tmux.send_keys 'C-t'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_and_change_preview_should_update_preview
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||||
|
tmux.send_keys 'C-t'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_sync
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys '00'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
# After 1 second
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case1
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case2
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case3
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case4
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case5
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 1001, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_disabled_case6
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
tmux.send_keys '9'
|
||||||
|
tmux.until { |lines| assert_equal 9, lines.match_count }
|
||||||
|
tmux.send_keys '9'
|
||||||
|
tmux.until { |lines| assert_equal 99, lines.match_count }
|
||||||
|
|
||||||
|
# TODO: How do we verify if an intermediate empty list is not shown?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_and_change
|
||||||
|
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_become_tty
|
||||||
|
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines, '/dev/tty' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_disabled_preview_update
|
||||||
|
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
|
||||||
|
tmux.send_keys :x
|
||||||
|
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_start_on_reload
|
||||||
|
tmux.send_keys %(echo foo | #{FZF} --header Loading --header-lines 1 --bind 'start:reload:sleep 2; echo bar' --bind 'load:change-header:Loaded' --bind space:change-header:), :Enter
|
||||||
|
tmux.until(timeout: 1) { |lines| assert_includes lines[-3], 'Loading' }
|
||||||
|
tmux.until(timeout: 1) { |lines| refute_includes lines[-4], 'foo' }
|
||||||
|
tmux.until { |lines| assert_includes lines[-3], 'Loaded' }
|
||||||
|
tmux.until { |lines| assert_includes lines[-4], 'bar' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_includes lines[-3], 'bar' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_become
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys 999
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 99, lines.item_count }
|
||||||
|
end
|
||||||
|
end
|
||||||
315
test/test_filter.rb
Normal file
315
test/test_filter.rb
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Non-interactive tests
|
||||||
|
class TestFilter < TestBase
|
||||||
|
def test_default_extended
|
||||||
|
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
|
||||||
|
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_exact
|
||||||
|
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
|
||||||
|
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
|
||||||
|
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_or_operator
|
||||||
|
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines(chomp: true)
|
||||||
|
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
|
||||||
|
`seq 10 | #{FZF} -f '1 | !1'`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_smart_case_for_each_term
|
||||||
|
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_filter_exitstatus
|
||||||
|
# filter / streaming filter
|
||||||
|
['', '--no-sort'].each do |opts|
|
||||||
|
assert_includes `echo foo | #{FZF} -f foo #{opts}`, 'foo'
|
||||||
|
assert_equal 0, $CHILD_STATUS.exitstatus
|
||||||
|
|
||||||
|
assert_empty `echo foo | #{FZF} -f bar #{opts}`
|
||||||
|
assert_equal 1, $CHILD_STATUS.exitstatus
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_long_line
|
||||||
|
data = '.' * 256 * 1024
|
||||||
|
File.open(tempname, 'w') do |f|
|
||||||
|
f << data
|
||||||
|
end
|
||||||
|
assert_equal data, `#{FZF} -f . < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_read0
|
||||||
|
lines = `find .`.lines(chomp: true)
|
||||||
|
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
|
||||||
|
assert_equal \
|
||||||
|
lines.last,
|
||||||
|
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nth_suffix_match
|
||||||
|
assert_equal \
|
||||||
|
'foo,bar,baz',
|
||||||
|
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_nth_basic
|
||||||
|
writelines(['hello world ', 'byebye'])
|
||||||
|
assert_equal \
|
||||||
|
'hello world ',
|
||||||
|
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_nth_template
|
||||||
|
writelines(['hello world ', 'byebye'])
|
||||||
|
assert_equal \
|
||||||
|
'hello world ',
|
||||||
|
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_nth_ansi
|
||||||
|
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||||
|
assert_equal \
|
||||||
|
'hello world ',
|
||||||
|
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_nth_no_ansi
|
||||||
|
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
|
||||||
|
writelines([src, 'byebye'])
|
||||||
|
assert_equal \
|
||||||
|
src,
|
||||||
|
`#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_escaped_meta_characters
|
||||||
|
input = [
|
||||||
|
'foo^bar',
|
||||||
|
'foo$bar',
|
||||||
|
'foo!bar',
|
||||||
|
"foo'bar",
|
||||||
|
'foo bar',
|
||||||
|
'bar foo'
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
|
||||||
|
assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
|
||||||
|
assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
|
||||||
|
assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal input.length - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.length
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_normalized_match
|
||||||
|
echoes = '(echo a; echo á; echo A; echo Á;)'
|
||||||
|
assert_equal %w[a á A Á], `#{echoes} | #{FZF} -f a`.lines.map(&:chomp)
|
||||||
|
assert_equal %w[á Á], `#{echoes} | #{FZF} -f á`.lines.map(&:chomp)
|
||||||
|
assert_equal %w[A Á], `#{echoes} | #{FZF} -f A`.lines.map(&:chomp)
|
||||||
|
assert_equal %w[Á], `#{echoes} | #{FZF} -f Á`.lines.map(&:chomp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unicode_case
|
||||||
|
writelines(%w[строКА1 СТРОКА2 строка3 Строка4])
|
||||||
|
assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak
|
||||||
|
input = %w[
|
||||||
|
--foobar--------
|
||||||
|
-----foobar---
|
||||||
|
----foobar--
|
||||||
|
-------foobar-
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
|
||||||
|
assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
by_length = %w[
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
-------foobar-
|
||||||
|
--foobar--------
|
||||||
|
]
|
||||||
|
assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
by_begin = %w[
|
||||||
|
--foobar--------
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
-------foobar-
|
||||||
|
]
|
||||||
|
assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||||
|
assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal %w[
|
||||||
|
-------foobar-
|
||||||
|
----foobar--
|
||||||
|
-----foobar---
|
||||||
|
--foobar--------
|
||||||
|
], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak_index_begin
|
||||||
|
writelines([
|
||||||
|
'xoxxxxxoxx',
|
||||||
|
'xoxxxxxox',
|
||||||
|
'xxoxxxoxx',
|
||||||
|
'xxxoxoxxx',
|
||||||
|
'xxxxoxox',
|
||||||
|
' xxoxoxxx'
|
||||||
|
])
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'xxxxoxox',
|
||||||
|
' xxoxoxxx',
|
||||||
|
'xxxoxoxxx',
|
||||||
|
'xxoxxxoxx',
|
||||||
|
'xoxxxxxox',
|
||||||
|
'xoxxxxxoxx'
|
||||||
|
], `#{FZF} -foo < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'xxxoxoxxx',
|
||||||
|
'xxxxoxox',
|
||||||
|
' xxoxoxxx',
|
||||||
|
'xxoxxxoxx',
|
||||||
|
'xoxxxxxoxx',
|
||||||
|
'xoxxxxxox'
|
||||||
|
], `#{FZF} -foo --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
# Note that --tiebreak=begin is now based on the first occurrence of the
|
||||||
|
# first character on the pattern
|
||||||
|
assert_equal [
|
||||||
|
' xxoxoxxx',
|
||||||
|
'xxxoxoxxx',
|
||||||
|
'xxxxoxox',
|
||||||
|
'xxoxxxoxx',
|
||||||
|
'xoxxxxxoxx',
|
||||||
|
'xoxxxxxox'
|
||||||
|
], `#{FZF} -foo --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
' xxoxoxxx',
|
||||||
|
'xxxoxoxxx',
|
||||||
|
'xxxxoxox',
|
||||||
|
'xxoxxxoxx',
|
||||||
|
'xoxxxxxox',
|
||||||
|
'xoxxxxxoxx'
|
||||||
|
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak_begin_algo_v2
|
||||||
|
writelines(['baz foo bar',
|
||||||
|
'foo bar baz'])
|
||||||
|
assert_equal [
|
||||||
|
'foo bar baz',
|
||||||
|
'baz foo bar'
|
||||||
|
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak_end
|
||||||
|
writelines(['xoxxxxxxxx',
|
||||||
|
'xxoxxxxxxx',
|
||||||
|
'xxxoxxxxxx',
|
||||||
|
'xxxxoxxxx',
|
||||||
|
'xxxxxoxxx',
|
||||||
|
' xxxxoxxx'])
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
' xxxxoxxx',
|
||||||
|
'xxxxoxxxx',
|
||||||
|
'xxxxxoxxx',
|
||||||
|
'xoxxxxxxxx',
|
||||||
|
'xxoxxxxxxx',
|
||||||
|
'xxxoxxxxxx'
|
||||||
|
], `#{FZF} -fo < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'xxxxxoxxx',
|
||||||
|
' xxxxoxxx',
|
||||||
|
'xxxxoxxxx',
|
||||||
|
'xxxoxxxxxx',
|
||||||
|
'xxoxxxxxxx',
|
||||||
|
'xoxxxxxxxx'
|
||||||
|
], `#{FZF} -fo --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'xxxxxoxxx',
|
||||||
|
' xxxxoxxx',
|
||||||
|
'xxxxoxxxx',
|
||||||
|
'xxxoxxxxxx',
|
||||||
|
'xxoxxxxxxx',
|
||||||
|
'xoxxxxxxxx'
|
||||||
|
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
writelines(['/bar/baz', '/foo/bar/baz'])
|
||||||
|
assert_equal [
|
||||||
|
'/foo/bar/baz',
|
||||||
|
'/bar/baz'
|
||||||
|
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak_length_with_nth
|
||||||
|
input = %w[
|
||||||
|
1:hell
|
||||||
|
123:hello
|
||||||
|
12345:he
|
||||||
|
1234567:h
|
||||||
|
]
|
||||||
|
writelines(input)
|
||||||
|
|
||||||
|
output = %w[
|
||||||
|
1:hell
|
||||||
|
12345:he
|
||||||
|
123:hello
|
||||||
|
1234567:h
|
||||||
|
]
|
||||||
|
assert_equal output, `#{FZF} -fh < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
# Since 0.16.8, --nth doesn't affect --tiebreak
|
||||||
|
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tiebreak_chunk
|
||||||
|
writelines(['1 foobarbaz ba',
|
||||||
|
'2 foobar baz',
|
||||||
|
'3 foo barbaz'])
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'3 foo barbaz',
|
||||||
|
'2 foobar baz',
|
||||||
|
'1 foobarbaz ba'
|
||||||
|
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'1 foobarbaz ba',
|
||||||
|
'2 foobar baz',
|
||||||
|
'3 foo barbaz'
|
||||||
|
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
'3 foo barbaz'
|
||||||
|
], `#{FZF} -f'!foobar' --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_boundary_match
|
||||||
|
# Underscore boundaries should be ranked lower
|
||||||
|
{
|
||||||
|
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
|
||||||
|
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
|
||||||
|
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
|
||||||
|
}.each do |scheme, expected|
|
||||||
|
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
|
||||||
|
assert_equal expected, result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
4323
test/test_go.rb
4323
test/test_go.rb
File diff suppressed because it is too large
Load Diff
994
test/test_layout.rb
Normal file
994
test/test_layout.rb
Normal file
@@ -0,0 +1,994 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Test cases that mainly use assert_block to verify the layout of fzf
|
||||||
|
class TestLayout < TestInteractive
|
||||||
|
def assert_block(expected, lines)
|
||||||
|
cols = expected.lines.map { it.chomp.length }.max
|
||||||
|
top = lines.take(expected.lines.length).map { it[0, cols].rstrip + "\n" }.join.chomp
|
||||||
|
bottom = lines.reverse.take(expected.lines.length).reverse.map { it[0, cols].rstrip + "\n" }.join.chomp
|
||||||
|
assert_includes [top, bottom], expected.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_vanilla
|
||||||
|
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
2
|
||||||
|
> 1
|
||||||
|
100000/100000
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
|
||||||
|
# Testing basic key bindings
|
||||||
|
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
|
||||||
|
block = <<~BLOCK
|
||||||
|
> 3910
|
||||||
|
391
|
||||||
|
856/100000
|
||||||
|
> 391
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal '3910', fzf_output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
|
||||||
|
block = <<~OUTPUT
|
||||||
|
> 4
|
||||||
|
997/997
|
||||||
|
>
|
||||||
|
3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
foobar
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_first_reverse
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
|
||||||
|
block = <<~OUTPUT
|
||||||
|
foobar
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
> < 997/997
|
||||||
|
> 4
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_and_transform_header
|
||||||
|
[
|
||||||
|
'space:change-header:$(seq 4)',
|
||||||
|
'space:transform-header:seq 4'
|
||||||
|
].each_with_index do |binding, i|
|
||||||
|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
bar
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
next unless i.zero?
|
||||||
|
|
||||||
|
teardown
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header
|
||||||
|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
bar
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_and_change_cache
|
||||||
|
tmux.send_keys "echo bar | #{FZF} --bind 'zero:change-header(foo)+reload(echo foo)+clear-query'", :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> bar
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
tmux.send_keys :z
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> foo
|
||||||
|
foo
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_toggle_header
|
||||||
|
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border rounded", :Enter
|
||||||
|
before = <<~OUTPUT
|
||||||
|
╭───────
|
||||||
|
│
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
│ 2/2
|
||||||
|
│ >
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ foo
|
||||||
|
╰───────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(before, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
after = <<~OUTPUT
|
||||||
|
╭───────
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
│ 2/2
|
||||||
|
│ >
|
||||||
|
╰───────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(after, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { assert_block(before, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_fit
|
||||||
|
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
╭──────────
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
│ > < 3/3
|
||||||
|
╰──────────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_fit_preview_above
|
||||||
|
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded --preview-window border-rounded --preview "seq {}" --preview-window up,60%', :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
╭──────────
|
||||||
|
│ ╭────────
|
||||||
|
│ │ 1
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ╰────────
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
│ > < 3/3
|
||||||
|
╰──────────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_fit_preview_above_alternative
|
||||||
|
tmux.send_keys 'seq 3 | fzf --height ~100% --border=sharp --preview "seq {}" --preview-window up,40%,border-bottom --padding 1 --exit-0 --header hello --header-lines=2', :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
┌─────────
|
||||||
|
│
|
||||||
|
│ 1
|
||||||
|
│ 2
|
||||||
|
│ 3
|
||||||
|
│ ───────
|
||||||
|
│ > 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ hello
|
||||||
|
│ 1/1 ─
|
||||||
|
│ >
|
||||||
|
│
|
||||||
|
└─────────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_fit_preview_left
|
||||||
|
tmux.send_keys "seq 3 | fzf --height ~100% --border=vertical --preview 'seq {}' --preview-window left,5,border-right --padding 1 --exit-0 --header $'hello\\nworld' --header-lines=2", :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
│
|
||||||
|
│ 1 │ > 3
|
||||||
|
│ 2 │ 2
|
||||||
|
│ 3 │ 1
|
||||||
|
│ │ hello
|
||||||
|
│ │ world
|
||||||
|
│ │ 1/1 ─
|
||||||
|
│ │ >
|
||||||
|
│
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_overflow
|
||||||
|
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
╭──────────────
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
│ > < 100/100
|
||||||
|
╰──────────────
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_extra_newline_issue_3209
|
||||||
|
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap,border-rounded --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
╭──────────
|
||||||
|
│ ─────────
|
||||||
|
│ something
|
||||||
|
│
|
||||||
|
╰──────────
|
||||||
|
3
|
||||||
|
2
|
||||||
|
> 1
|
||||||
|
100/100 ─
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fzf_multi_line
|
||||||
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ┃998
|
||||||
|
│ ┃999
|
||||||
|
│ ┃1000
|
||||||
|
│ ╹
|
||||||
|
│ ╻1
|
||||||
|
│ ╹2
|
||||||
|
│ >>0
|
||||||
|
│ 3/3 (3)
|
||||||
|
│ >
|
||||||
|
╰───────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
tmux.send_keys :Up, :Up
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭───────
|
||||||
|
│ >╻1
|
||||||
|
│ >┃2
|
||||||
|
│ >┃3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ >┃
|
||||||
|
│
|
||||||
|
│ >
|
||||||
|
╰───
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fzf_multi_line_reverse
|
||||||
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded --reverse], :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭───────────
|
||||||
|
│ >
|
||||||
|
│ 3/3 (3)
|
||||||
|
│ >>0
|
||||||
|
│ ╻1
|
||||||
|
│ ╹2
|
||||||
|
│ ╻1
|
||||||
|
│ ┃2
|
||||||
|
│ ┃3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fzf_multi_line_no_pointer_and_marker
|
||||||
|
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded --reverse --pointer '' --marker '' --marker-multi-line ''], :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭───────────
|
||||||
|
│ >
|
||||||
|
│ 3/3 (3)
|
||||||
|
│ 0
|
||||||
|
│ 1
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ 2
|
||||||
|
│ 3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_gap
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --gap --border rounded --reverse), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭─────────────────
|
||||||
|
│ >
|
||||||
|
│ 100/100 ──────
|
||||||
|
│ > 1
|
||||||
|
│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
||||||
|
│ 2
|
||||||
|
│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
||||||
|
│ 3
|
||||||
|
│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
||||||
|
│ 4
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_gap_2
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --gap=2 --gap-line xyz --border rounded --reverse), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭─────────────────
|
||||||
|
│ >
|
||||||
|
│ 100/100 ──────
|
||||||
|
│ > 1
|
||||||
|
│
|
||||||
|
│ xyzxyzxyzxyzxy
|
||||||
|
│ 2
|
||||||
|
│
|
||||||
|
│ xyzxyzxyzxyzxy
|
||||||
|
│ 3
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_list_border_and_label
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --query 1 --padding 1,2), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ║ 3
|
||||||
|
│ ║ 2
|
||||||
|
│ ║ 1
|
||||||
|
│ ║ 19/97 ─
|
||||||
|
│ ║ > 1
|
||||||
|
│ ╚list══════
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_input_border_and_label
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --input-border bold --input-label input --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ 11
|
||||||
|
│ > 10
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ ┏input━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_input_border_and_label_header_first
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --input-border bold --input-label input --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 --header-first), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ 11
|
||||||
|
│ > 10
|
||||||
|
│ ┏input━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_list_input_border_and_label
|
||||||
|
tmux.send_keys %(
|
||||||
|
seq 100 | #{FZF} --border rounded --list-border double --input-border bold --list-label-pos 2:bottom --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 \
|
||||||
|
--bind 'start:transform-input-label(echo INPUT)+transform-list-label(echo LIST)' \
|
||||||
|
--bind 'space:change-input-label( input )+change-list-label( list )'
|
||||||
|
).strip, :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚LIST══════
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ ┏INPUT━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚ list ════
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ ┏ input ━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_list_input_border_and_label_header_first
|
||||||
|
tmux.send_keys %(
|
||||||
|
seq 100 | #{FZF} --border rounded --list-border double --input-border bold --list-label-pos 2:bottom --input-label-pos 2 --header-lines 3 --query 1 --padding 1,2 \
|
||||||
|
--bind 'start:transform-input-label(echo INPUT)+transform-list-label(echo LIST)' \
|
||||||
|
--bind 'space:change-input-label( input )+change-list-label( list )' --header-first
|
||||||
|
).strip, :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚LIST══════
|
||||||
|
│ ┏INPUT━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚ list ════
|
||||||
|
│ ┏ input ━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗━━━━━━━━━━
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_and_label
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ 12
|
||||||
|
│ 11
|
||||||
|
│ > 10
|
||||||
|
│ ┌────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header──
|
||||||
|
│ 19/97 ─
|
||||||
|
│ > 1
|
||||||
|
│
|
||||||
|
╰────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_toggle
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header-border rounded --bind 'space:change-header(hello),enter:change-header()'), :Enter
|
||||||
|
block1 = <<~BLOCK
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
│ 100/100 ─
|
||||||
|
│ >
|
||||||
|
╰────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
╰────────────
|
||||||
|
╭────────────
|
||||||
|
│ hello
|
||||||
|
╰────────────
|
||||||
|
100/100 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_toggle_with_header_lines
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header-border rounded --bind 'space:change-header(hello),enter:change-header()' --header-lines 2), :Enter
|
||||||
|
block1 = <<~BLOCK
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╭──────────
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
╰──────────
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╭──────────
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ hello
|
||||||
|
╰──────────
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_toggle_with_header_lines_header_first
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header-border rounded --bind 'space:change-header(hello),enter:change-header()' --header-lines 2 --header-first), :Enter
|
||||||
|
block1 = <<~BLOCK
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
╭──────────
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
╰──────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
╭──────────
|
||||||
|
│ 2
|
||||||
|
│ 1
|
||||||
|
│ hello
|
||||||
|
╰──────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_toggle_with_header_lines_header_lines_border
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header-border rounded --bind 'space:change-header(hello),enter:change-header()' --header-lines 2 --header-lines-border double), :Enter
|
||||||
|
block1 = <<~BLOCK
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╔══════════
|
||||||
|
║ 2
|
||||||
|
║ 1
|
||||||
|
╚══════════
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╔══════════
|
||||||
|
║ 2
|
||||||
|
║ 1
|
||||||
|
╚══════════
|
||||||
|
╭──────────
|
||||||
|
│ hello
|
||||||
|
╰──────────
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_toggle_with_header_lines_header_first_header_lines_border
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --list-border rounded --header-border rounded --bind 'space:change-header(hello),enter:change-header()' --header-lines 2 --header-first --header-lines-border double), :Enter
|
||||||
|
block1 = <<~BLOCK
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╔══════════
|
||||||
|
║ 2
|
||||||
|
║ 1
|
||||||
|
╚══════════
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Space
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
│ > 3
|
||||||
|
╰──────────
|
||||||
|
╔══════════
|
||||||
|
║ 2
|
||||||
|
║ 1
|
||||||
|
╚══════════
|
||||||
|
98/98 ─
|
||||||
|
>
|
||||||
|
╭──────────
|
||||||
|
│ hello
|
||||||
|
╰──────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { assert_block(block1, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_and_label_header_first
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --header-first), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ 12
|
||||||
|
│ 11
|
||||||
|
│ > 10
|
||||||
|
│ 19/97 ─
|
||||||
|
│ > 1
|
||||||
|
│ ┌────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header──
|
||||||
|
│
|
||||||
|
╰────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_and_label_with_list_border
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 12
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚list══════
|
||||||
|
│ ┌──────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header────
|
||||||
|
│ 19/97 ─
|
||||||
|
│ > 1
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_header_border_and_label_with_list_border_header_first
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --header-first), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 12
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚list══════
|
||||||
|
│ 19/97 ─
|
||||||
|
│ > 1
|
||||||
|
│ ┌──────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header────
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_all_borders
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --input-border bold --input-label input --input-label-pos 2:bottom), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 12
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚list══════
|
||||||
|
│ ┌──────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header────
|
||||||
|
│ ┏━━━━━━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗input━━━━━
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_all_borders_header_first
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border rounded --list-border double --list-label list --list-label-pos 2:bottom --header-lines 3 --header-border sharp --header-label header --header-label-pos 2:bottom --query 1 --padding 1,2 --input-border bold --input-label input --input-label-pos 2:bottom --header-first), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ║ 12
|
||||||
|
│ ║ 11
|
||||||
|
│ ║ > 10
|
||||||
|
│ ╚list══════
|
||||||
|
│ ┏━━━━━━━━━━
|
||||||
|
│ ┃ 19/97
|
||||||
|
│ ┃ > 1
|
||||||
|
│ ┗input━━━━━
|
||||||
|
│ ┌──────────
|
||||||
|
│ │ 3
|
||||||
|
│ │ 2
|
||||||
|
│ │ 1
|
||||||
|
│ └header────
|
||||||
|
│
|
||||||
|
╰──────────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_style_full_adaptive_height
|
||||||
|
tmux.send_keys %(seq 1| #{FZF} --style=full:rounded --height=~100% --header-lines=1 --info=default), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╭────────
|
||||||
|
╰────────
|
||||||
|
╭────────
|
||||||
|
│ 1
|
||||||
|
╰────────
|
||||||
|
╭────────
|
||||||
|
│ 0/0
|
||||||
|
│ >
|
||||||
|
╰────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_style_full_adaptive_height_double
|
||||||
|
tmux.send_keys %(seq 1| #{FZF} --style=full:double --border --height=~100% --header-lines=1 --info=default), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
╔══════════
|
||||||
|
║ ╔════════
|
||||||
|
║ ╚════════
|
||||||
|
║ ╔════════
|
||||||
|
║ ║ 1
|
||||||
|
║ ╚════════
|
||||||
|
║ ╔════════
|
||||||
|
║ ║ 0/0
|
||||||
|
║ ║ >
|
||||||
|
║ ╚════════
|
||||||
|
╚══════════
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_noinfo
|
||||||
|
# │ 1 ││
|
||||||
|
tmux.send_keys %(#{FZF} --preview 'seq 1000' --preview-window top,noinfo --scrollbar --bind space:change-preview-window:info), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines[1]&.start_with?('│ 1')
|
||||||
|
assert lines[1]&.end_with?(' ││')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines[1]&.start_with?('│ 1')
|
||||||
|
assert lines[1]&.end_with?('1000││')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_min_height_no_auto
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --border sharp --style full:sharp --height 1% --min-height 5), :Enter
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
┌───────
|
||||||
|
│ ┌─────
|
||||||
|
│ │ >
|
||||||
|
│ └─────
|
||||||
|
└───────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_min_height_auto
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --style full:sharp --height 1% --min-height 5+), :Enter
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
┌─────────
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
└─────────
|
||||||
|
┌─────────
|
||||||
|
│ >
|
||||||
|
└─────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_min_height_auto_no_input
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --style full:sharp --no-input --height 1% --min-height 5+), :Enter
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
┌─────────
|
||||||
|
│ 5
|
||||||
|
│ 4
|
||||||
|
│ 3
|
||||||
|
│ 2
|
||||||
|
│ > 1
|
||||||
|
└─────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_min_height_auto_no_input_reverse_list
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --style full:sharp --layout reverse-list --no-input --height 1% --min-height 5+ --bind a:show-input,b:hide-input,c:toggle-input), :Enter
|
||||||
|
|
||||||
|
block = <<~BLOCK
|
||||||
|
┌─────────
|
||||||
|
│ > 1
|
||||||
|
│ 2
|
||||||
|
│ 3
|
||||||
|
│ 4
|
||||||
|
│ 5
|
||||||
|
└─────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
tmux.send_keys :a
|
||||||
|
block2 = <<~BLOCK
|
||||||
|
┌─────
|
||||||
|
│ > 1
|
||||||
|
│ 2
|
||||||
|
└─────
|
||||||
|
┌─────
|
||||||
|
│ >
|
||||||
|
└─────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
tmux.send_keys :b
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
tmux.send_keys :c
|
||||||
|
tmux.until { assert_block(block2, it) }
|
||||||
|
tmux.send_keys :c
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_layout_reverse_list
|
||||||
|
prefix = "seq 5 | #{FZF} --layout reverse-list --no-list-border --height ~100% --border sharp "
|
||||||
|
suffixes = [
|
||||||
|
%(),
|
||||||
|
%[--header "$(seq 101 103)"],
|
||||||
|
%[--header "$(seq 101 103)" --header-first],
|
||||||
|
%[--header "$(seq 101 103)" --header-lines 3],
|
||||||
|
%[--header "$(seq 101 103)" --header-lines 3 --header-first],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-first],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-lines 3],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-lines 3 --header-lines-border sharp],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-lines 3 --header-lines-border sharp --header-first],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-lines 3 --header-lines-border sharp --header-first --input-border sharp],
|
||||||
|
%[--header "$(seq 101 103)" --header-border sharp --header-lines 3 --header-lines-border sharp --header-first --no-input],
|
||||||
|
%[--header "$(seq 101 103)" --input-border sharp],
|
||||||
|
%[--header "$(seq 101 103)" --style full:sharp],
|
||||||
|
%[--header "$(seq 101 103)" --style full:sharp --header-first]
|
||||||
|
]
|
||||||
|
output = <<~BLOCK
|
||||||
|
┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌──────── ┌───────── ┌─────── ┌───────── ┌───────── ┌─────────
|
||||||
|
│ > 1 │ > 1 │ > 1 │ 1 │ 1 │ > 1 │ > 1 │ 1 │ ┌────── │ ┌────── │ ┌─────── │ ┌───── │ > 1 │ ┌─────── │ ┌───────
|
||||||
|
│ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ │ 1 │ │ 1 │ │ 1 │ │ 1 │ 2 │ │ > 1 │ │ > 1
|
||||||
|
│ 3 │ 3 │ 3 │ 3 │ 3 │ 3 │ 3 │ 3 │ │ 2 │ │ 2 │ │ 2 │ │ 2 │ 3 │ │ 2 │ │ 2
|
||||||
|
│ 4 │ 4 │ 4 │ > 4 │ > 4 │ 4 │ 4 │ > 4 │ │ 3 │ │ 3 │ │ 3 │ │ 3 │ 4 │ │ 3 │ │ 3
|
||||||
|
│ 5 │ 5 │ 5 │ 5 │ 5 │ 5 │ 5 │ 5 │ └────── │ └────── │ └─────── │ └───── │ 5 │ │ 4 │ │ 4
|
||||||
|
│ 5/5 ─ │ 101 │ 5/5 ─ │ 101 │ 2/2 ─ │ ┌────── │ 5/5 ─ │ ┌────── │ > 4 │ > 4 │ > 4 │ > 4 │ 101 │ │ 5 │ │ 5
|
||||||
|
│ > │ 102 │ > │ 102 │ > │ │ 101 │ > │ │ 101 │ 5 │ 5 │ 5 │ 5 │ 102 │ └─────── │ └───────
|
||||||
|
└──────── │ 103 │ 101 │ 103 │ 101 │ │ 102 │ ┌────── │ │ 102 │ ┌────── │ 2/2 ─ │ ┌─────── │ ┌───── │ 103 │ ┌─────── │ ┌───────
|
||||||
|
│ 5/5 ─ │ 102 │ 2/2 ─ │ 102 │ │ 103 │ │ 101 │ │ 103 │ │ 101 │ > │ │ 2/2 │ │ 101 │ ┌─────── │ │ 101 │ │ >
|
||||||
|
│ > │ 103 │ > │ 103 │ └────── │ │ 102 │ └────── │ │ 102 │ ┌────── │ │ > │ │ 102 │ │ 5/5 │ │ 102 │ └───────
|
||||||
|
└──────── └──────── └──────── └──────── │ 5/5 ─ │ │ 103 │ 2/2 ─ │ │ 103 │ │ 101 │ └─────── │ │ 103 │ │ > │ │ 103 │ ┌───────
|
||||||
|
│ > │ └────── │ > │ └────── │ │ 102 │ ┌─────── │ └───── │ └─────── │ └─────── │ │ 101
|
||||||
|
└──────── └──────── └──────── │ 2/2 ─ │ │ 103 │ │ 101 └─────── └───────── │ ┌─────── │ │ 102
|
||||||
|
│ > │ └────── │ │ 102 │ │ > │ │ 103
|
||||||
|
└──────── └──────── │ │ 103 │ └─────── │ └───────
|
||||||
|
│ └─────── └───────── └─────────
|
||||||
|
└─────────
|
||||||
|
BLOCK
|
||||||
|
|
||||||
|
expects = []
|
||||||
|
output.each_line.first.scan(/\S+/) do
|
||||||
|
offset = Regexp.last_match.offset(0)
|
||||||
|
expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
suffixes.zip(expects).each do |suffix, block|
|
||||||
|
tmux.send_keys(prefix + suffix, :Enter)
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
|
||||||
|
teardown
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header_and_label_at_once
|
||||||
|
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
|
||||||
|
block = <<~BLOCK
|
||||||
|
│ ┌─label──
|
||||||
|
│ │ header
|
||||||
|
│ └────────
|
||||||
|
│ 10/10 ─
|
||||||
|
│ >
|
||||||
|
└──────────
|
||||||
|
BLOCK
|
||||||
|
tmux.until { assert_block(block, it) }
|
||||||
|
end
|
||||||
|
end
|
||||||
561
test/test_preview.rb
Normal file
561
test/test_preview.rb
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Test cases for preview
|
||||||
|
class TestPreview < TestInteractive
|
||||||
|
def test_preview
|
||||||
|
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {-} ' }
|
||||||
|
tmux.send_keys '555'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], ' {555-555} ' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert lines[-2]&.start_with?(' 28/1000 ') }
|
||||||
|
tmux.send_keys 'foobar'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], ' {55-55} ' }
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {-1} ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {3-1 } ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {4-1 3} ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_toggle_preview_without_default_preview_command
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
refute_includes lines[1], '║ [1]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# toggle-preview should do nothing
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
refute_includes lines[1], '║ [1]'
|
||||||
|
refute_includes lines[1], '║ [2]'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 3'
|
||||||
|
refute_includes lines[1], '║ [3]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# One-off preview action
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
|
||||||
|
|
||||||
|
# toggle-preview to hide it
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
|
||||||
|
|
||||||
|
# toggle-preview again does nothing
|
||||||
|
tmux.send_keys :Enter, :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 4'
|
||||||
|
refute_includes lines[1], '║ [4]'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_and_hide_preview
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
|
||||||
|
|
||||||
|
# Hidden by default
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
refute_includes lines[1], '┃ [1]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
|
||||||
|
|
||||||
|
# Already shown
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
|
||||||
|
|
||||||
|
# Hide
|
||||||
|
tmux.send_keys :b
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 3'
|
||||||
|
refute_includes lines[1], '┃ [3]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Already hidden
|
||||||
|
tmux.send_keys :b
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 4'
|
||||||
|
refute_includes lines[1], '┃ [4]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show it again
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_hidden
|
||||||
|
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
||||||
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_match(/ {1-1-1-[0-9]+}/, lines[-2]) }
|
||||||
|
tmux.send_keys '555'
|
||||||
|
tmux.until { |lines| assert_match(/ {555-555-1-[0-9]+}/, lines[-2]) }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_equal '> 555', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_size_0
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
assert_equal ' 100/100', lines[1]
|
||||||
|
assert_equal '> 1', lines[2]
|
||||||
|
end
|
||||||
|
wait do
|
||||||
|
assert_path_exists tempname
|
||||||
|
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space, :Down, :Down
|
||||||
|
tmux.until { |lines| assert_equal '> 3', lines[4] }
|
||||||
|
wait do
|
||||||
|
assert_path_exists tempname
|
||||||
|
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space, :Down
|
||||||
|
tmux.until { |lines| assert_equal '> 4', lines[5] }
|
||||||
|
wait do
|
||||||
|
assert_path_exists tempname
|
||||||
|
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_size_0_hidden
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys :Down, :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 3' }
|
||||||
|
wait { refute_path_exists tempname }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait do
|
||||||
|
assert_path_exists tempname
|
||||||
|
assert_equal %w[3], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Down
|
||||||
|
wait do
|
||||||
|
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space, :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 6' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait do
|
||||||
|
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_flags
|
||||||
|
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
||||||
|
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 //0/0} ' }
|
||||||
|
tmux.send_keys '123'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {////123//} ' }
|
||||||
|
tmux.send_keys 'C-u', '1'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 /1/0/0} ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1/1 /1/9/0} ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1 10/1 10 /1/9/0 9} ' }
|
||||||
|
tmux.send_keys '2'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /12//0 9} ' }
|
||||||
|
tmux.send_keys '3'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_file
|
||||||
|
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||||
|
tmux.send_keys :BTab
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo barbar foobarfoo0101 ' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_q_no_match
|
||||||
|
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||||
|
tmux.send_keys 'bar'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_q_no_match_with_initial_query
|
||||||
|
tmux.send_keys %(: | #{FZF} --preview 'echo 1. /{q}/{q:1}/; echo 2. /{q:..}/{q:2}/{q:-1}/; echo 3. /{q:s-2}/{q:-2}/{q:x}/' --query 'foo bar'), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '1. /foo bar/foo/' }
|
||||||
|
tmux.until { |lines| assert_includes lines[2], '2. /foo bar/bar/bar/' }
|
||||||
|
tmux.until { |lines| assert_includes lines[3], '3. /foo /foo/{q:x}/' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_update_on_select
|
||||||
|
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
|
||||||
|
:Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert(lines.any? { |line| line.include?(' 1 2 3 4 5 ') }) }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| lines.each { |line| refute_includes line, ' 1 2 3 4 5 ' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_correct_tab_width_after_ansi_reset_code
|
||||||
|
writelines(["\x1b[31m+\x1b[m\t\x1b[32mgreen"])
|
||||||
|
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines[1], ' + green ' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_bindings_with_default_preview
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||||
|
tmux.until { |lines| lines.match_count == 10 }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||||
|
tmux.send_keys 'c'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||||
|
tmux.send_keys 'b'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[2]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_bindings_without_default_preview
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||||
|
tmux.until { |lines| lines.match_count == 10 }
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||||
|
tmux.send_keys 'c' # does nothing
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||||
|
tmux.send_keys 'b'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||||
|
tmux.send_keys 9
|
||||||
|
tmux.until { |lines| lines.match_count == 1 }
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '2' }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_scroll_begin_constant
|
||||||
|
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter
|
||||||
|
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||||
|
tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_scroll_begin_expr
|
||||||
|
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter
|
||||||
|
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||||
|
tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_scroll_begin_and_offset
|
||||||
|
['echo foo 123 321', 'echo foo :123: 321'].each do |input|
|
||||||
|
tmux.send_keys "#{input} | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter
|
||||||
|
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||||
|
tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_clear_screen
|
||||||
|
tmux.send_keys %{seq 100 | #{FZF} --preview 'for i in $(seq 300); do (( i % 200 == 0 )) && printf "\\033[2J"; echo "[$i]"; sleep 0.001; done'}, :Enter
|
||||||
|
tmux.until { |lines| lines.match_count == 100 }
|
||||||
|
tmux.until { |lines| lines[1]&.include?('[200]') }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_follow
|
||||||
|
file = Tempfile.new('fzf-follow')
|
||||||
|
file.sync = true
|
||||||
|
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
|
||||||
|
tmux.until { |lines| lines.match_count == 100 }
|
||||||
|
|
||||||
|
# Write to the temporary file, and check if the preview window is showing
|
||||||
|
# the last line of the file
|
||||||
|
tmux.until { |lines| assert_includes lines[1], 'start' }
|
||||||
|
3.times { file.puts _1 } # header lines
|
||||||
|
1000.times { file.puts _1 }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '/1004' }
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '999' }
|
||||||
|
|
||||||
|
# Scroll the preview window and fzf should stop following the file content
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '998' }
|
||||||
|
file.puts 'foo', 'bar'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '/1006'
|
||||||
|
assert_includes lines[-2], '998'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Scroll back to the bottom and fzf should start following the file again
|
||||||
|
%w[999 foo bar].each do |item|
|
||||||
|
wait do
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], item }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
file.puts 'baz'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '/1007'
|
||||||
|
assert_includes lines[-2], 'baz'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Scroll upwards to stop following
|
||||||
|
tmux.send_keys :Up
|
||||||
|
wait { assert_includes lines[-2], 'bar' }
|
||||||
|
file.puts 'aaa'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '/1008'
|
||||||
|
assert_includes lines[-2], 'bar'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Manually enable following
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
|
||||||
|
file.puts 'bbb'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '/1009'
|
||||||
|
assert_includes lines[-2], 'bbb'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Disable following
|
||||||
|
tmux.send_keys :Space
|
||||||
|
file.puts 'ccc', 'ddd'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '/1011'
|
||||||
|
assert_includes lines[-2], 'bbb'
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
file.close
|
||||||
|
file.unlink
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_toggle_preview_wrap
|
||||||
|
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
|
||||||
|
2.times do
|
||||||
|
tmux.until { |lines| assert_includes lines[2], '2nd line' }
|
||||||
|
tmux.send_keys 'C-w'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[2], 'wrapped'
|
||||||
|
assert_includes lines[3], '2nd line'
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-w'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_close
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[1], 'foo' }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], 'foo' }
|
||||||
|
tmux.send_keys '10'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
tmux.send_keys 'C-l', 'closed'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_header
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '4/1000'
|
||||||
|
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys '55'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-J'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 19, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-K'
|
||||||
|
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_preview_window
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --no-preview-border --bind '" \
|
||||||
|
'a:change-preview(echo __{}__),' \
|
||||||
|
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
|
||||||
|
'c:change-preview(),d:change-preview-window(hidden),' \
|
||||||
|
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||||
|
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
|
||||||
|
|
||||||
|
# change-preview action permanently changes the preview command set by --preview
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[0], '__1__' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[0], '__2__' }
|
||||||
|
|
||||||
|
# When multiple change-preview-window actions are bound to a single key,
|
||||||
|
# the last one wins and the updated options are immediately applied to the new preview
|
||||||
|
tmux.send_keys 'b'
|
||||||
|
tmux.until { |lines| assert_equal '==2==', lines[0] }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_equal '==3==', lines[0] }
|
||||||
|
|
||||||
|
# change-preview with an empty preview command closes the preview window
|
||||||
|
tmux.send_keys 'c'
|
||||||
|
tmux.until { |lines| refute_includes lines[0], '==' }
|
||||||
|
|
||||||
|
# change-preview again to re-open the preview window
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_equal '__3__', lines[0] }
|
||||||
|
|
||||||
|
# Hide the preview window with hidden flag
|
||||||
|
tmux.send_keys 'd'
|
||||||
|
tmux.until { |lines| refute_includes lines[0], '__3__' }
|
||||||
|
|
||||||
|
# One-off preview
|
||||||
|
tmux.send_keys 'e'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal '::', lines[0]
|
||||||
|
refute_includes lines[1], '3'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Wrapped
|
||||||
|
tmux.send_keys 'f'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal '::', lines[0]
|
||||||
|
assert_equal '↳ 3', lines[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_preview_window_should_not_reset_change_preview
|
||||||
|
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines, 'hello' }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines, '│ hello' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_preview_window_rotate
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
|
||||||
|
"a:change-preview-window(right|down|up|hidden|)'", :Enter
|
||||||
|
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
|
||||||
|
3.times do
|
||||||
|
tmux.until { |lines| lines[0].start_with?('hello') }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| lines[0].end_with?('hello') }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| lines[-1].start_with?('hello') }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_equal 'hello', lines[0] }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| refute_includes lines[0], 'hello' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_preview_window_rotate_hidden
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
|
||||||
|
"a:change-preview-window(nohidden||down,1|)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||||
|
tmux.send_keys 'a'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_preview_window_rotate_hidden_down
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||||
|
tmux.send_keys '?'
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_toggle_alternative_preview_window
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
tmux.until { |lines| refute_includes lines, '/1/1/' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_alternative_preview_window_opts
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_width_exception
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --scrollbar --preview-window border-left --border --preview 'seq 1000'", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines[1]&.end_with?(' 1/1000││')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_window_hidden_on_focus
|
||||||
|
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_preview_query_should_not_be_affected_by_search
|
||||||
|
tmux.send_keys "seq 1 | #{FZF} --bind 'change:transform-search(echo {q:1})' --preview 'echo [{q}/{}]'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys '1'
|
||||||
|
tmux.until { |lines| assert lines.any_include?('[1/1]') }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert lines.any_include?('[1 /1]') }
|
||||||
|
tmux.send_keys '2'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert lines.any_include?('[1 2/1]')
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
52
test/test_server.rb
Normal file
52
test/test_server.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Test cases for API server
|
||||||
|
class TestServer < TestInteractive
|
||||||
|
def test_listen
|
||||||
|
{ '--listen 6266' => -> { URI('http://localhost:6266') },
|
||||||
|
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
|
||||||
|
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||||
|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||||
|
assert_equal 10, state[:totalCount]
|
||||||
|
assert_equal 10, state[:matchCount]
|
||||||
|
assert_empty state[:query]
|
||||||
|
assert_equal({ index: 0, text: '1' }, state[:current])
|
||||||
|
|
||||||
|
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] }
|
||||||
|
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||||
|
assert_equal 100, state[:totalCount]
|
||||||
|
assert_equal 0, state[:matchCount]
|
||||||
|
assert_equal 'yo', state[:query]
|
||||||
|
assert_nil state[:current]
|
||||||
|
|
||||||
|
teardown
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_listen_with_api_key
|
||||||
|
post_uri = URI('http://localhost:6266')
|
||||||
|
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||||
|
# Incorrect API Key
|
||||||
|
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
||||||
|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||||
|
assert_equal '401', res.code
|
||||||
|
assert_equal 'Unauthorized', res.message
|
||||||
|
assert_equal "invalid api key\n", res.body
|
||||||
|
end
|
||||||
|
# Valid API Key
|
||||||
|
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
||||||
|
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||||
|
assert_equal '200', res.code
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
485
test/test_shell_integration.rb
Normal file
485
test/test_shell_integration.rb
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'lib/common'
|
||||||
|
|
||||||
|
# Testing shell integration
|
||||||
|
module TestShell
|
||||||
|
attr_reader :tmux
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@tmux = Tmux.new(shell)
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
@tmux.kill
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_var(name, val)
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "export #{name}='#{val}'", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
|
||||||
|
def unset_var(name)
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "unset #{name}", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ctrl_t
|
||||||
|
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'C-t'
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||||
|
tmux.send_keys :Tab, :Tab, :Tab
|
||||||
|
tmux.until { |lines| assert lines.any_include?(' (3)') }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines.any_include?('1 2 3') }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ctrl_t_unicode
|
||||||
|
writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
|
||||||
|
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo ', 'C-t'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.send_keys 'fzf-unicode'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys '1'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys '2'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_alt_c
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys :Escape, :c
|
||||||
|
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys :pwd, :Enter
|
||||||
|
tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_alt_c_command
|
||||||
|
set_var('FZF_ALT_C_COMMAND', 'echo /tmp')
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /', :Enter
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys :Escape, :c
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys :pwd, :Enter
|
||||||
|
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ctrl_r
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo 1st', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo 2nd', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'echo 3d', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
3.times do
|
||||||
|
tmux.send_keys 'echo 3rd', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
tmux.send_keys 'echo 4th', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys 'e3d'
|
||||||
|
# Duplicates removed: 3d (1) + 3rd (1) => 2 matches
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal '3rd', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ctrl_r_multiline
|
||||||
|
# NOTE: Current bash implementation shows an extra new line if there's
|
||||||
|
# only entry in the history
|
||||||
|
tmux.send_keys ':', :Enter
|
||||||
|
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||||
|
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
|
tmux.send_keys 'foo bar'
|
||||||
|
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
|
||||||
|
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ctrl_r_abort
|
||||||
|
skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
|
||||||
|
%w[foo ' "].each do |query|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys :Enter, query
|
||||||
|
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||||
|
tmux.send_keys 'C-r'
|
||||||
|
tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
|
||||||
|
tmux.send_keys 'C-g'
|
||||||
|
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module CompletionTest
|
||||||
|
def test_file_completion
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf test')
|
||||||
|
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
|
||||||
|
['no~such~user', '/tmp/fzf test/foobar'].each do |f|
|
||||||
|
FileUtils.touch(File.expand_path(f))
|
||||||
|
end
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys ' !d'
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
tmux.send_keys :Tab, :Tab
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) do |lines|
|
||||||
|
assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
# ~USERNAME**<TAB>
|
||||||
|
user = `whoami`.chomp
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.send_keys "cat ~#{user}**", :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys "/#{user}"
|
||||||
|
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) do |lines|
|
||||||
|
assert_match %r{cat .*/#{user}}, lines[-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
# ~INVALID_USERNAME**<TAB>
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.send_keys 'cat ~such**', :Tab
|
||||||
|
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
|
||||||
|
|
||||||
|
# /tmp/fzf\ test**<TAB>
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys 'foobar$'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?('> /tmp/fzf test/foobar')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
|
||||||
|
|
||||||
|
# Should include hidden files
|
||||||
|
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
||||||
|
tmux.until(true) do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
assert lines.any_include?('/tmp/fzf-test/.hidden-')
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
ensure
|
||||||
|
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
|
||||||
|
FileUtils.rm_rf(File.expand_path(f))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_file_completion_root
|
||||||
|
tmux.send_keys 'ls /**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dir_completion
|
||||||
|
(1..100).each do |idx|
|
||||||
|
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
|
||||||
|
end
|
||||||
|
FileUtils.touch('/tmp/fzf-test/d55/xxx')
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys :Tab, :Tab # Tab does not work here
|
||||||
|
tmux.send_keys 55
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> 55'
|
||||||
|
assert_includes lines, '> /tmp/fzf-test/d55/'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||||
|
tmux.send_keys :xx
|
||||||
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||||
|
|
||||||
|
# Should not match regular files (bash-only)
|
||||||
|
if instance_of?(TestBash)
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fail back to plusdirs
|
||||||
|
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_process_completion
|
||||||
|
tmux.send_keys 'sleep 12345 &', :Enter
|
||||||
|
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
|
||||||
|
pid = lines[-1]&.split&.last
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'C-L'
|
||||||
|
tmux.send_keys 'kill **', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys 'sleep12345'
|
||||||
|
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
|
||||||
|
ensure
|
||||||
|
if pid
|
||||||
|
begin
|
||||||
|
Process.kill('KILL', pid.to_i)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_custom_completion
|
||||||
|
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'ls /tmp/**', :Tab
|
||||||
|
tmux.until { |lines| assert_equal 11, lines.match_count }
|
||||||
|
tmux.send_keys :Tab, :Tab, :Tab
|
||||||
|
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unset_completion
|
||||||
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
|
||||||
|
# Using tmux
|
||||||
|
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
|
||||||
|
# FZF_TMUX=1
|
||||||
|
new_shell
|
||||||
|
tmux.focus
|
||||||
|
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_completion_in_command_sequence
|
||||||
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
|
tmux.prepare
|
||||||
|
|
||||||
|
triggers = ['**', '~~', '++', 'ff', '/']
|
||||||
|
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
|
||||||
|
|
||||||
|
triggers.each do |trigger|
|
||||||
|
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
||||||
|
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
||||||
|
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_file_completion_unicode
|
||||||
|
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||||
|
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'cat fzf-unicode**', :Tab
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys '1'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||||
|
|
||||||
|
tmux.send_keys '2'
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||||
|
tmux.send_keys :Tab
|
||||||
|
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_custom_completion_api
|
||||||
|
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
|
||||||
|
%w[f g].each do |command|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "#{command} b**", :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.item_count
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert lines.any_include?("prompt-#{command}")
|
||||||
|
assert lines.any_include?("preview-#{command}-bar")
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
|
||||||
|
tmux.send_keys 'C-u'
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'unset -f _fzf_comprun', :Enter
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ssh_completion
|
||||||
|
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
|
||||||
|
|
||||||
|
tmux.send_keys 'ssh jg@localhost**', :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_operator lines.match_count, :>=, 1
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
|
||||||
|
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_operator lines.match_count, :>=, 5
|
||||||
|
assert_equal 0, lines.select_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Tab, :Tab, :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 3, lines.select_count
|
||||||
|
end
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
|
||||||
|
|
||||||
|
tmux.send_keys 'localhost**', :Tab
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_operator lines.match_count, :>=, 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TestBash < TestBase
|
||||||
|
include TestShell
|
||||||
|
include CompletionTest
|
||||||
|
|
||||||
|
def shell
|
||||||
|
:bash
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_shell
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dynamic_completion_loader
|
||||||
|
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
||||||
|
tmux.paste '_completion_loader() { complete -o default fake; }'
|
||||||
|
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
|
||||||
|
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'fake /tmp/foo'
|
||||||
|
tmux.send_keys :Tab, 'C-u'
|
||||||
|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||||
|
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TestZsh < TestBase
|
||||||
|
include TestShell
|
||||||
|
include CompletionTest
|
||||||
|
|
||||||
|
def shell
|
||||||
|
:zsh
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_shell
|
||||||
|
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complete_quoted_command
|
||||||
|
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||||
|
['unset', '\unset', "'unset'"].each do |command|
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "#{command} FZFFOOBR**", :Tab
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
|
||||||
|
tmux.send_keys 'C-c'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TestFish < TestBase
|
||||||
|
include TestShell
|
||||||
|
|
||||||
|
def shell
|
||||||
|
:fish
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_shell
|
||||||
|
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
|
||||||
|
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||||
|
tmux.until { |lines| assert_empty lines }
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_var(name, val)
|
||||||
|
tmux.prepare
|
||||||
|
tmux.send_keys "set -g #{name} '#{val}'", :Enter
|
||||||
|
tmux.prepare
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,7 +13,7 @@ Execute (fzf#run with dir option):
|
|||||||
|
|
||||||
execute 'lcd' fnameescape(cwd)
|
execute 'lcd' fnameescape(cwd)
|
||||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
AssertEqual ['fzf.vader'], result
|
||||||
AssertEqual 1, haslocaldir()
|
AssertEqual 1, haslocaldir()
|
||||||
AssertEqual getcwd(), cwd
|
AssertEqual getcwd(), cwd
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ Execute (fzf#run with Funcref command):
|
|||||||
call add(g:ret, a:e)
|
call add(g:ret, a:e)
|
||||||
endfunction
|
endfunction
|
||||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
AssertEqual ['fzf.vader'], result
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
AssertEqual ['fzf.vader'], sort(g:ret)
|
||||||
|
|
||||||
Execute (fzf#run with string source):
|
Execute (fzf#run with string source):
|
||||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||||
@@ -78,18 +78,18 @@ Execute (fzf#wrap):
|
|||||||
|
|
||||||
let opts = fzf#wrap('foobar')
|
let opts = fzf#wrap('foobar')
|
||||||
Log opts
|
Log opts
|
||||||
AssertEqual '~40%', opts.down
|
AssertEqual 0.9, opts.window.width
|
||||||
Assert opts.options =~ '--expect='
|
Assert opts.options =~ '--expect='
|
||||||
Assert !has_key(opts, 'sink')
|
Assert !has_key(opts, 'sink')
|
||||||
Assert has_key(opts, 'sink*')
|
Assert has_key(opts, 'sink*')
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {}, 0)
|
let opts = fzf#wrap('foobar', {}, 0)
|
||||||
Log opts
|
Log opts
|
||||||
AssertEqual '~40%', opts.down
|
AssertEqual 0.9, opts.window.width
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {}, 1)
|
let opts = fzf#wrap('foobar', {}, 1)
|
||||||
Log opts
|
Log opts
|
||||||
Assert !has_key(opts, 'down')
|
Assert !has_key(opts, 'window')
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {'down': '50%'})
|
let opts = fzf#wrap('foobar', {'down': '50%'})
|
||||||
Log opts
|
Log opts
|
||||||
@@ -148,7 +148,7 @@ Execute (fzf#wrap):
|
|||||||
|
|
||||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||||
let opts = fzf#wrap({})
|
let opts = fzf#wrap({})
|
||||||
Assert opts.options =~ '^--color=fg:'
|
Assert opts.options =~ '--color=fg:'
|
||||||
|
|
||||||
Execute (fzf#shellescape with sh):
|
Execute (fzf#shellescape with sh):
|
||||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||||
Reference in New Issue
Block a user