m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 06:13:47 -05:00

Compare commits

..

38 Commits

Author SHA1 Message Date
Junegunn Choi
4138333f5c 0.27.3 2021-10-15 23:59:56 +09:00
Vlastimil Ovčáčík
61339a8ae2 Add more tests of placeholder flags and simplify its logic (#2624)
* [tests] Test fzf's placeholders and escaping on practical commands

This tests some reasonable commands in fzf's templates (for commands,
previews, rebinds etc.), how are those commands escaped (backslashes,
double quotes), and documents if the output is executable in cmd.exe.
Both on Unix and Windows.

* [tests] Add testing of placeholder parsing and matching

Adds tests and bit of docs for the curly brackets placeholders in fzf's
template strings. Also tests the "placeholder" regex.

* [tests] Add more test cases of replacing placeholders focused on flags

Replacing placeholders in templates is already tested, this adds tests
that focus more on the parameters of placeholders - e.g. flags, token
ranges.

There is at least one test for each flag, not all combinations are
tested though.

* [refactoring] Split OS-specific function quoteEntry() to corresponding source file

This is minor refactoring, and also the function's test was made
crossplatform.

* [refactoring] Simplify replacePlaceholder function

Should be equivalent to the original, but has simpler structure.
2021-10-15 22:31:59 +09:00
Junegunn Choi
50eb2e3855 Render spinner on info line during "reload"
Fix #2637
2021-10-15 22:13:57 +09:00
Xeonacid
5fc78e4584 Add riscv64 build target (#2626)
Build successfully on Arch Linux RISC-V.
2021-10-10 21:33:06 +09:00
Junegunn Choi
2736a2f69e [vim] Empty out $FZF_DEFAULT_COMMAND before unletting it
For Vim 8.0.1831 and below
* https://github.com/vim/vim/issues/1116

Fix https://github.com/junegunn/fzf.vim/issues/1301
2021-10-10 20:19:48 +09:00
Vlastimil Ovčáčík
179993f0cd Enable manually trigger on GitHub Workflows (#2620) 2021-10-04 21:41:54 +09:00
dependabot[bot]
b734f657f9 Bump github.com/mattn/go-isatty from 0.0.12 to 0.0.14 (#2612)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.12 to 0.0.14.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.12...v0.0.14)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-04 21:13:46 +09:00
dependabot[bot]
c29d7d02c2 Bump github.com/mattn/go-shellwords from 1.0.11 to 1.0.12 (#2592)
Bumps [github.com/mattn/go-shellwords](https://github.com/mattn/go-shellwords) from 1.0.11 to 1.0.12.
- [Release notes](https://github.com/mattn/go-shellwords/releases)
- [Commits](https://github.com/mattn/go-shellwords/compare/v1.0.11...v1.0.12)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-shellwords
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-04 21:12:40 +09:00
dependabot[bot]
3df6b2a58c Bump github.com/mattn/go-runewidth from 0.0.12 to 0.0.13 (#2591)
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.12 to 0.0.13.
- [Release notes](https://github.com/mattn/go-runewidth/releases)
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.12...v0.0.13)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-04 21:01:03 +09:00
Vlastimil Ovčáčík
b8aa2d2c32 Minor refactoring tcell library from tui.go to tcell.go
To prevent including tcell library in non-windows builds.
2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
0ff885461b Add mouse support to the FullscreenRenderer 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
ca43f95fb1 Fix Backspace key to emit BSpace and AltBS events instead of CtrlH
CtrlH events are still sent when appropriate. I have adjusted
FullscreenRenderer to match the LightRenderer's behaviour, which seems
to be correct.
2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
09700f676b Add CtrlCaret keyboard event to FullscreenRenderer 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
4271e9cffa Fix Ctrl+Space key combination to emit CtrlSpace instead of Rune ' ' 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
f3dc8a10d5 Add ability to type AltGr characters in FullscreenRenderer on Windows. 2021-10-03 01:39:30 +09:00
Vlastimil Ovčáčík
00fb486f6a [tests] Add testing of keyboard events in FullscreenRenderer.GetChar()
This contains one test case of each tcell.Key* event type that can be
sent to and subsequently processed in fzf's GetChar(). The test cases
describe status quo, and all of them PASS.

Small function util.ToTty() was added. It is similar to util.IsTty(),
but for stdout (hence the To preposition).
2021-10-03 01:39:30 +09:00
Junegunn Choi
4173e94c6f Do not check for --height support on --version
https://github.com/junegunn/fzf.vim/issues/1329
2021-09-29 20:17:44 +09:00
Hiroki Konishi
261d3d3340 fix: replace broken links with archived ones 2021-09-28 18:07:22 +09:00
Hiroki Konishi
15e20fcae1 fix: spelling Refence -> Reference 2021-09-28 18:07:22 +09:00
Vlastimil Ovčáčík
f4f47f5fe3 Minor changes
- obsolete todo removed, I tested the ev.ch for " " char and it works just
fine
2021-09-24 16:04:36 +09:00
Vlastimil Ovčáčík
71d11de7ca [tests] Change tests to output to stdout only with verbose flag
This hides stdout output unless "go test -v" was run.
2021-09-24 16:04:36 +09:00
Vlastimil Ovčáčík
88d74a15aa Change the tests to run on Windows (#2615)
Most of the "expected" strings in terminal.go test were changed to
"text/template" values. Quotes in those string were parametrized in
the templates. Two functions handling templates were added
for convenience.

Templates has the advantage of:
- parametrize repetitive strings inside "expected" values
  - inner and outer quotes were parametrized in templates
  - long and confusing test values are more readable
- templates can be localized for other operating systems
2021-09-24 09:45:06 +09:00
Junegunn Choi
0f02fc0c77 Reset {n} after reload
Fix #2611
2021-09-14 20:36:10 +09:00
Keating950
3f90fb42d8 Fix spelling error (Extention -> Extension) (#2589) 2021-08-17 16:40:24 +09:00
Daniel Bast
9bd8988300 Add dependabot config for dependency updates (#2573) 2021-08-17 16:27:50 +09:00
a1346054
3c804bcfec fix spelling 2021-08-15 16:03:26 +09:00
a1346054
cca4cdc4f1 improve test logic and be explicit about the test 2021-08-15 16:03:26 +09:00
a1346054
8f899aaf8a use proper bash-style notation 2021-08-15 16:03:26 +09:00
a1346054
e53b4bb439 always use [[ ... ]] and not [ ... ] in bash completions 2021-08-15 16:03:26 +09:00
a1346054
ab247a1309 use consistent style for bash [[ ... ]] 2021-08-15 16:03:26 +09:00
Michael Kelley
c21e9edad4 Restore VT hack for Windows (#2580)
- restore VT enable hack
- resolve an issue reported in https://github.com/kelleyma49/PSFzf
2021-08-15 16:01:50 +09:00
Leon Tepe
9c21a20f8b Minor readme change (#2578)
`CTRL-K` moves up and `CTRL-J` moves down, not the other way around (same for `CTRL-P` and `CTRL-N`
2021-08-15 15:57:07 +09:00
Junegunn Choi
7191ebb615 Do not show preview window by default if --preview is empty
Close #2516
2021-06-08 08:53:29 +09:00
Junegunn Choi
a74731d7f5 [vim] Add 'sinklist' as a synonym to 'sink*'
So that it's easier to add a sinklist function to a spec dictionary.

  let spec = { 'source': source, 'options': ['--preview', preview] }
  function spec.sinklist(matches)
    echom string(a:matches)
  endfunction

  call fzf#run(fzf#wrap(spec))
2021-06-04 22:07:29 +09:00
Junegunn Choi
e086f0b3fe 0.27.2 2021-06-01 17:00:24 +09:00
Junegunn Choi
8255aa23f4 Fix bug where --read0 not properly displaying long lines
Fix #2508
2021-06-01 16:55:51 +09:00
Junegunn Choi
a4bc08f5a3 Allow specifying 16 base ANSI colors by their names
Close #2502
2021-05-26 19:35:26 +09:00
Thomas Klausner
7e5aa1e2a5 Mention NetBSD package and how to install it (#2499)
Close #2487
2021-05-23 11:31:55 +09:00
37 changed files with 1233 additions and 206 deletions

6
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -6,6 +6,7 @@ on:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
analyze:

View File

@@ -6,6 +6,7 @@ on:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:

View File

@@ -6,6 +6,7 @@ on:
branches: [ master, devel ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:

View File

@@ -190,7 +190,7 @@ list without restarting fzf.
### Toggling between data sources
You're not limiited to just one reload binding. Set up multiple bindings so
You're not limited to just one reload binding. Set up multiple bindings so
you can switch between data sources.
```sh
@@ -349,7 +349,7 @@ IFS=: read -ra selected < <(
fzf can kill the initial Ripgrep process it starts with the initial query.
Otherwise, the initial Ripgrep process will keep consuming system resources
even after `reload` is triggered.
- Filtering is no longer a responsibitiliy of fzf; hence `--disabled`
- Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt.
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
reduce the number of intermediate Ripgrep processes while we're typing in

View File

@@ -1,6 +1,35 @@
CHANGELOG
=========
0.27.3
------
- Preview window is `hidden` by default when there are `preview` bindings but
`--preview` command is not given
- Fixed bug where `{n}` is not properly reset on `reload`
- Fixed bug where spinner is not displayed on `reload`
- Enhancements in tcell renderer for Windows (#2616)
- Vim plugin
- `sinklist` is added as a synonym to `sink*` so that it's easier to add
a function to a spec dictionary
```vim
let spec = { 'source': 'ls', 'options': ['--multi', '--preview', 'cat {}'] }
function spec.sinklist(matches)
echom string(a:matches)
endfunction
call fzf#run(fzf#wrap(spec))
```
- Vim 7 compatibility
0.27.2
------
- 16 base ANSI colors can be specified by their names
```sh
fzf --color fg:3,fg+:11
fzf --color fg:yellow,fg+:bright-yellow
```
- Fix bug where `--read0` not properly displaying long lines
0.27.1
------
- Added `unbind` action. In the following Ripgrep launcher example, you can
@@ -12,7 +41,7 @@ CHANGELOG
" fzf will read the stream file while allowing other processes to append to it
call fzf#run({'source': 'cat /dev/null > /tmp/stream; tail -f /tmp/stream'})
```
- It is now possible to open popup window relative to the currrent window
- It is now possible to open popup window relative to the current window
```vim
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
```
@@ -62,7 +91,7 @@ CHANGELOG
```
- Added `select` and `deselect` action for unconditionally selecting or
deselecting a single item in `--multi` mode. Complements `toggle` action.
- Sigificant performance improvement in ANSI code processing
- Significant performance improvement in ANSI code processing
- Bug fixes and improvements
- Built with Go 1.16
@@ -1144,4 +1173,3 @@ add `--sync` option to re-enable buffering.
### Improvements
- `--select-1` and `--exit-0` will start finder immediately when the condition
cannot be met

View File

@@ -34,6 +34,7 @@ BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
BINARYRISCV64 := fzf-$(GOOS)_riscv64
# https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m)
@@ -59,6 +60,8 @@ else ifeq ($(UNAME_M),aarch64)
BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),ppc64le)
BINARY := $(BINARYPPC64LE)
else ifeq ($(UNAME_M),riscv64)
BINARY := $(BINARYRISCV64)
else
$(error Build on $(UNAME_M) is not supported, yet.)
endif
@@ -141,6 +144,9 @@ target/$(BINARYARM8): $(SOURCES)
target/$(BINARYPPC64LE): $(SOURCES)
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYRISCV64): $(SOURCES)
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf

View File

@@ -283,7 +283,7 @@ The following table summarizes the available options.
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
@@ -387,7 +387,7 @@ command! -bang -complete=dir -nargs=? LS
- `g:fzf_layout`
- `g:fzf_action`
- **Works only when no custom `sink` (or `sink*`) is provided**
- **Works only when no custom `sink` (or `sinklist`) is provided**
- Having custom sink usually means that each entry is not an ordinary
file path (e.g. name of color scheme), so we can't blindly apply the
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)

View File

@@ -121,6 +121,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
| Pacman | Arch Linux | `sudo pacman -S fzf` |
| pkg | FreeBSD | `pkg install fzf` |
| pkgin | NetBSD | `pkgin install fzf` |
| pkg_add | OpenBSD | `pkg_add fzf` |
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
| Zypper | openSUSE | `sudo zypper install fzf` |
@@ -201,7 +202,7 @@ vim $(fzf)
#### Using the finder
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
- Emacs style key bindings

View File

@@ -172,7 +172,7 @@ cleanup() {
resize-pane -Z
fi
if [ $# -gt 0 ]; then
if [[ $# -gt 0 ]]; then
trap - EXIT
exit 130
fi

View File

@@ -300,7 +300,7 @@ The following table summarizes the available options.
`source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf
`dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )

8
go.mod
View File

@@ -3,13 +3,13 @@ module github.com/junegunn/fzf
require (
github.com/gdamore/tcell v1.4.0
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.12
github.com/mattn/go-shellwords v1.0.11
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.2.0
github.com/saracen/walker v0.1.2
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
golang.org/x/text v0.3.6 // indirect
)

18
go.sum
View File

@@ -5,14 +5,13 @@ github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404Z
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw=
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
@@ -21,10 +20,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -2,7 +2,7 @@
set -u
version=0.27.1
version=0.27.3
auto_completion=
key_bindings=
update_config=2

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "May 2021" "fzf 0.27.1" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Oct 2021" "fzf 0.27.3" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "May 2021" "fzf 0.27.1" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Oct 2021" "fzf 0.27.3" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -340,6 +340,22 @@ color mappings.
\fB-1 \fRDefault terminal foreground/background color
\fB \fR(or the original color of the text)
\fB0 ~ 15 \fR16 base colors
\fBblack\fR
\fBred\fR
\fBgreen\fR
\fByellow\fR
\fBblue\fR
\fBmagenta\fR
\fBcyan\fR
\fBwhite\fR
\fBbright-black\fR (gray | grey)
\fBbright-red\fR
\fBbright-green\fR
\fBbright-yellow\fR
\fBbright-blue\fR
\fBbright-magenta\fR
\fBbright-cyan\fR
\fBbright-white\fR
\fB16 ~ 255 \fRANSI 256 colors
\fB#rrggbb \fR24-bit colors

View File

@@ -159,7 +159,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin)
return s:versions[a:bin]
end
let command = a:bin . ' --version'
let command = a:bin . ' --version --no-height'
let output = systemlist(command)
if v:shell_error || empty(output)
return ''
@@ -419,13 +419,13 @@ function! fzf#wrap(...)
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
if !s:has_any(opts, ['sink', 'sinklist', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
function! opts.sinklist(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
let opts['sink*'] = opts.sinklist " For backward compatibility
endif
return opts
@@ -519,7 +519,8 @@ finally
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
execute 'unlet $FZF_DEFAULT_COMMAND'
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
@@ -943,6 +944,8 @@ function! s:callback(dict, lines) abort
endif
if has_key(a:dict, 'sink*')
call a:dict['sink*'](a:lines)
elseif has_key(a:dict, 'sinklist')
call a:dict['sinklist'](a:lines)
endif
catch
if stridx(v:exception, ':E325:') < 0

View File

@@ -35,9 +35,9 @@ fi
bind '"\e[0n": redraw-current-line'
__fzf_comprun() {
if [ "$(type -t _fzf_comprun 2>&1)" = function ]; then
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then
elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then
shift
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
else
@@ -140,14 +140,14 @@ _fzf_handle_dynamic_completion() {
orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd"
orig="${!orig_var##*#}"
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
$orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then
elif [[ -n "$_fzf_completion_loader" ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@"
ret=$?
# _completion_loader may not have updated completion for the command
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
@@ -171,17 +171,17 @@ __fzf_generic_path_completion() {
[[ $base = *"/"* ]] && dir="$base"
while true; do
if [ -z "$dir" ] || [ -d "$dir" ]; then
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
[[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
printf "%q$3 " "$item"
done)
matches=${matches% }
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [ -n "$matches" ]; then
if [[ -n "$matches" ]]; then
COMPREPLY=( "$matches" )
else
COMPREPLY=( "$cur" )
@@ -234,7 +234,7 @@ _fzf_complete() {
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [ -n "$selected" ]; then
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")
else
COMPREPLY=("$cur")
@@ -330,7 +330,7 @@ __fzf_defc() {
opts="$3"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}"
if [ -n "$orig" ]; then
if [[ -n "$orig" ]]; then
printf -v def "$orig" "$func"
eval "$def"
else

View File

@@ -27,7 +27,7 @@ __fzf_select__() {
if [[ $- =~ i ]]; then
__fzfcmd() {
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } &&
[[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
}
@@ -52,7 +52,7 @@ __fzf_history__() {
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [ -z "$READLINE_POINT" ]; then
if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
@@ -66,7 +66,7 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode'
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
if (( BASH_VERSINFO[0] < 4 )); then
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'

View File

@@ -107,7 +107,7 @@ type Result struct {
const (
scoreMatch = 16
scoreGapStart = -3
scoreGapExtention = -1
scoreGapExtension = -1
// We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over
@@ -125,16 +125,16 @@ const (
// Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtention
bonusCamel123 = bonusBoundary + scoreGapExtension
// Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
// The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is
// still respected.
bonusFirstCharMultiplier = 2
@@ -424,7 +424,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
inGap = false
} else {
if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
} else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
}
@@ -477,7 +477,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
var s1, s2, consecutive int16
if inGap {
s2 = Hleft[off] + scoreGapExtention
s2 = Hleft[off] + scoreGapExtension
} else {
s2 = Hleft[off] + scoreGapStart
}
@@ -598,7 +598,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
pidx++
} else {
if inGap {
score += scoreGapExtention
score += scoreGapExtension
} else {
score += scoreGapStart
}

View File

@@ -43,10 +43,10 @@ func TestFuzzyMatch(t *testing.T) {
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
for _, forward := range []bool{true, false} {
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
@@ -54,18 +54,18 @@ func TestFuzzyMatch(t *testing.T) {
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
scoreGapStart*2+scoreGapExtention*3)
scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
@@ -73,13 +73,13 @@ func TestFuzzyMatch(t *testing.T) {
bonusNonWord+bonusBoundary)
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
scoreGapStart*2+scoreGapExtention*4)
scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtention*2)
scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
util.Max(bonusCamel123, bonusBoundary))
@@ -99,7 +99,7 @@ func TestFuzzyMatch(t *testing.T) {
func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtention)
scoreGapStart+scoreGapExtension)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
}

View File

@@ -1,7 +1,6 @@
package fzf
import (
"fmt"
"math/rand"
"regexp"
"strings"
@@ -17,11 +16,13 @@ import (
//
// References:
// - https://github.com/gnachman/iTerm2
// - http://ascii-table.com/ansi-escape-sequences.php
// - http://ascii-table.com/ansi-escape-sequences-vt-100.php
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexRefence = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
func testParserReference(t testing.TB, str string) {
t.Helper()
@@ -36,7 +37,7 @@ func testParserReference(t testing.TB, str string) {
s := str
for i := 0; ; i++ {
got := toSlice(nextAnsiEscapeSequence(s))
exp := ansiRegexRefence.FindStringIndex(s)
exp := ansiRegexReference.FindStringIndex(s)
equal := len(got) == len(exp)
if equal {
@@ -207,7 +208,7 @@ func TestExtractColor(t *testing.T) {
if output != "hello world" {
t.Errorf("Invalid output: %s %v", output, []rune(output))
}
fmt.Println(src, ansiOffsets, clean)
t.Log(src, ansiOffsets, clean)
assertion(ansiOffsets, state)
}
@@ -409,7 +410,7 @@ func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
a := ansiRegexRefence.FindStringIndex(s)
a := ansiRegexReference.FindStringIndex(s)
if len(a) == 0 {
break
}

View File

@@ -233,6 +233,7 @@ func Run(opts *Options, version string, revision string) {
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear()
itemIndex = 0
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}

View File

@@ -670,6 +670,38 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
cattr.Attr |= tui.Blink
case "reverse":
cattr.Attr |= tui.Reverse
case "black":
cattr.Color = tui.Color(0)
case "red":
cattr.Color = tui.Color(1)
case "green":
cattr.Color = tui.Color(2)
case "yellow":
cattr.Color = tui.Color(3)
case "blue":
cattr.Color = tui.Color(4)
case "magenta":
cattr.Color = tui.Color(5)
case "cyan":
cattr.Color = tui.Color(6)
case "white":
cattr.Color = tui.Color(7)
case "bright-black", "gray", "grey":
cattr.Color = tui.Color(8)
case "bright-red":
cattr.Color = tui.Color(9)
case "bright-green":
cattr.Color = tui.Color(10)
case "bright-yellow":
cattr.Color = tui.Color(11)
case "bright-blue":
cattr.Color = tui.Color(12)
case "bright-magenta":
cattr.Color = tui.Color(13)
case "bright-cyan":
cattr.Color = tui.Color(14)
case "bright-white":
cattr.Color = tui.Color(15)
case "":
default:
if rrggbb.MatchString(component) {
@@ -1560,7 +1592,7 @@ func validateSign(sign string, signOptName string) error {
}
func postProcessOptions(opts *Options) {
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform")
}
// Default actions for CTRL-N / CTRL-P when --history is set

View File

@@ -23,6 +23,26 @@ import (
// import "github.com/pkg/profile"
/*
Placeholder regex is used to extract placeholders from fzf's template
strings. Acts as input validation for parsePlaceholder function.
Describes the syntax, but it is fairly lenient.
The following pseudo regex has been reverse engineered from the
implementation. It is overly strict, but better describes whats possible.
As such it is not useful for validation, but rather to generate test
cases for example.
\\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type
|{q} # query type
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
)
RANGE = (?:
(?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y)
|-?[0-9]+ # shorthand syntax (x..x)
)
*/
var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
@@ -425,6 +445,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delay = initialDelay
}
var previewBox *util.EventBox
showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
previewBox = util.NewEventBox()
}
@@ -521,7 +542,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
@@ -1519,22 +1540,6 @@ func keyMatch(key tui.Event, event tui.Event) bool {
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
}
func quoteEntryCmd(entry string) string {
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
}
func quoteEntry(entry string) string {
if util.IsWindows() {
return quoteEntryCmd(entry)
}
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags := placeholderFlags{}
@@ -1560,6 +1565,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
skipChars++
case 'q':
flags.query = true
// query flag is not skipped
default:
break
}
@@ -1647,50 +1653,41 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
if selected[0] == nil {
selected = []*Item{}
}
// replace placeholders one by one
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
escaped, match, flags := parsePlaceholder(match)
if escaped {
// this function implements the effects a placeholder has on items
var replace func(*Item) string
// placeholder types (escaped, query type, item type, token type)
switch {
case escaped:
return match
}
// Current query
if match == "{q}" {
case match == "{q}":
return quoteEntry(query)
}
items := current
if flags.plus || forcePlus {
items = selected
}
replacements := make([]string, len(items))
if match == "{}" {
for idx, item := range items {
if flags.number {
case match == "{}":
replace = func(item *Item) string {
switch {
case flags.number:
n := int(item.text.Index)
if n < 0 {
replacements[idx] = ""
} else {
replacements[idx] = strconv.Itoa(n)
return ""
}
} else if flags.file {
replacements[idx] = item.AsString(stripAnsi)
} else {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
return strconv.Itoa(n)
case flags.file:
return item.AsString(stripAnsi)
default:
return quoteEntry(item.AsString(stripAnsi))
}
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ")
}
tokens := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(tokens))
for idx, s := range tokens {
r, ok := ParseRange(&s)
default:
// token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(rangeExpressions))
for idx, s := range rangeExpressions {
r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
if !ok {
// Invalid expression, just return the original string in the template
return match
@@ -1698,26 +1695,44 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
ranges[idx] = r
}
for idx, item := range items {
replace = func(item *Item) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := joinTokens(trans)
// trim the last delimiter
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1)
// make sure the delimiter is at the very end of the string
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]]
}
}
if !flags.preserveSpace {
str = strings.TrimSpace(str)
}
if !flags.file {
str = quoteEntry(str)
}
replacements[idx] = str
return str
}
}
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || forcePlus {
items = selected
}
replacements := make([]string, len(items))
for idx, item := range items {
replacements[idx] = replace(item)
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
@@ -2657,6 +2672,7 @@ func (t *Terminal) Loop() {
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
t.reading = true
}
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")

View File

@@ -1,18 +1,17 @@
package fzf
import (
"bytes"
"io"
"os"
"regexp"
"strings"
"testing"
"text/template"
"github.com/junegunn/fzf/src/util"
)
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
@@ -30,75 +29,96 @@ func TestReplacePlaceholder(t *testing.T) {
t.Errorf("expected: %s, actual: %s", expected, result)
}
}
// helper function that converts template format into string and carries out the check()
checkFormat := func(format string) {
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
unixStyle := quotes{`'`, `'\''`, "\n"}
windowsStyle := quotes{`^"`, `'`, "\n"}
var effectiveStyle quotes
if util.IsWindows() {
effectiveStyle = windowsStyle
} else {
effectiveStyle = unixStyle
}
expected := templateToString(format, effectiveStyle)
check(expected)
}
printsep := "\n"
/*
Test multiple placeholders and the function parameters.
*/
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar baz'")
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar baz'")
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'")
checkFormat("echo {{.O}} foo{{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo 'bar baz'")
checkFormat("echo {{.O}}bar baz{{.O}}")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
checkFormat("echo {{.O}} {{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ''\\'''")
checkFormat("echo {{.O}}{{.I}}{{.O}}")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
checkFormat("echo {{.O}} {{.O}}")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
@@ -106,34 +126,425 @@ func TestReplacePlaceholder(t *testing.T) {
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter
regex = regexp.MustCompile("[oa]+")
// foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/*
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
see: TestParsePlaceholder
*/
items3 := []*Item{
// single line
newItem("1a 1b 1c 1d 1e 1f"),
// multi line
newItem("1a 1b 1c 1d 1e 1f"),
newItem("2a 2b 2c 2d 2e 2f"),
newItem("3a 3b 3c 3d 3e 3f"),
newItem("4a 4b 4c 4d 4e 4f"),
newItem("5a 5b 5c 5d 5e 5f"),
newItem("6a 6b 6c 6d 6e 6f"),
newItem("7a 7b 7c 7d 7e 7f"),
}
stripAnsi := false
printsep = "\n"
forcePlus := false
query := "sample query"
templateToOutput := make(map[string]string)
templateToFile := make(map[string]string) // same as above, but the file contents will be matched
// I. item type placeholder
templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
templateToOutput[`{n}`] = `0`
templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
templateToFile[`{nf}`] = `0{{.S}}`
templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
// II. token type placeholders
templateToOutput[`{..}`] = templateToOutput[`{}`]
templateToOutput[`{1..}`] = templateToOutput[`{}`]
templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
// shorthand for x..x range
templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
templateToOutput[`{-6}`] = templateToOutput[`{1}`]
// multiple ranges
templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
// flags
templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
templateToFile[`{f1}`] = `1a{{.S}}`
templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
// IV. escaping placeholder
templateToOutput[`\{}`] = `{}`
templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput {
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
checkFormat(wantOutput)
}
for giveTemplate, wantOutput := range templateToFile {
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
data, err := readFile(path)
if err != nil {
t.Errorf("Cannot read the content of the temp file %s.", path)
}
result = string(data)
checkFormat(wantOutput)
}
}
func TestQuoteEntry(t *testing.T) {
type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes
if util.IsWindows() {
effectiveStyle = windowsStyle
} else {
effectiveStyle = unixStyle
}
func TestQuoteEntryCmd(t *testing.T) {
tests := map[string]string{
`"`: `^"\^"^"`,
`\`: `^"\\^"`,
`\"`: `^"\\\^"^"`,
`"\\\"`: `^"\^"\\\\\\\^"^"`,
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
`'`: `{{.O}}{{.SQ}}{{.O}}`,
`"`: `{{.O}}{{.DQ}}{{.O}}`,
`\`: `{{.O}}{{.BS}}{{.O}}`,
`\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
`"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
`$`: `{{.O}}${{.O}}`,
`$HOME`: `{{.O}}$HOME{{.O}}`,
`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
`&`: `{{.O}}{{.E}}&{{.O}}`,
`|`: `{{.O}}{{.E}}|{{.O}}`,
`<`: `{{.O}}{{.E}}<{{.O}}`,
`>`: `{{.O}}{{.E}}>{{.O}}`,
`(`: `{{.O}}{{.E}}({{.O}}`,
`)`: `{{.O}}{{.E}}){{.O}}`,
`@`: `{{.O}}{{.E}}@{{.O}}`,
`^`: `{{.O}}{{.E}}^{{.O}}`,
`%`: `{{.O}}{{.E}}%{{.O}}`,
`!`: `{{.O}}{{.E}}!{{.O}}`,
`%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
`C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
`"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
}
for input, expected := range tests {
escaped := quoteEntryCmd(input)
escaped := quoteEntry(input)
expected = templateToString(expected, effectiveStyle)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
}
}
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
func TestUnixCommands(t *testing.T) {
if util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
// 1) working examples
// paths that does not have to evaluated will work fine, when quoted
{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
// quoting the tilde is required (to be treated as string)
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
// 2) problematic examples
// paths that need to expand some part of it won't work (special characters and variables)
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
}
testCommands(t, tests)
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) {
if !util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
// 1) working examples
// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
// 2) problematic examples
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
// cat: "C:\\test.txt: Invalid argument
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
// cat: "C:\\test.txt": Invalid argument
{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
// the temp file contains: `cat "C:\test.txt"`
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
}
testCommands(t, tests)
}
/*
Test typical valid placeholders and parsing of them.
Also since the parser assumes the input is matched with `placeholder` regex,
the regex is tested here as well.
*/
func TestParsePlaceholder(t *testing.T) {
// give, want pairs
templates := map[string]string{
// I. item type placeholder
`{}`: `{}`,
`{+}`: `{+}`,
`{n}`: `{n}`,
`{+n}`: `{+n}`,
`{f}`: `{f}`,
`{+nf}`: `{+nf}`,
// II. token type placeholders
`{..}`: `{..}`,
`{1..}`: `{1..}`,
`{..2}`: `{..2}`,
`{1..2}`: `{1..2}`,
`{-2..-1}`: `{-2..-1}`,
// shorthand for x..x range
`{1}`: `{1}`,
`{1..1}`: `{1..1}`,
`{-6}`: `{-6}`,
// multiple ranges
`{1,2}`: `{1,2}`,
`{1,2,4}`: `{1,2,4}`,
`{1,2..4}`: `{1,2..4}`,
`{1..2,-4..-3}`: `{1..2,-4..-3}`,
// flags
`{+1}`: `{+1}`,
`{+-1}`: `{+-1}`,
`{s1}`: `{s1}`,
`{f1}`: `{f1}`,
`{+s1..2}`: `{+s1..2}`,
`{+sf1..2}`: `{+sf1..2}`,
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
`{q}`: `{qq}`,
// IV. escaping placeholder
`\{}`: `{}`,
`\{++}`: `{++}`,
`{++}`: `{+}`,
}
for giveTemplate, wantTemplate := range templates {
if !placeholder.MatchString(giveTemplate) {
t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
continue
}
_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
if gotTemplate != wantTemplate {
t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
}
}
}
/* utilities section */
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
// Functions tested in this file require array of items (allItems). The array needs
// to consist of at least two nils. This is helper function.
func newItems(str ...string) []*Item {
result := make([]*Item, util.Max(len(str), 2))
for i, s := range str {
result[i] = newItem(s)
}
return result
}
// (for logging purposes)
func (item *Item) String() string {
return item.AsString(true)
}
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
func templateToString(format string, data interface{}) string {
bb := &bytes.Buffer{}
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
if err != nil {
panic(err)
}
return bb.String()
}
// ad hoc types for test cases
type give struct {
template string
query string
allItems []*Item
}
type want struct {
/*
Unix:
The `want.output` string is supposed to be formatted for evaluation by
`sh -c command` system call.
Windows:
The `want.output` string is supposed to be formatted for evaluation by
`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
behaviour, which is more favourable for nesting (possibly escaped)
special characters. This is the relevant section of `help cmd`:
...old behavior is to see if the first character is
a quote character and if so, strip the leading character and
remove the last quote character on the command line, preserving
any text after the last quote character.
*/
output string // literal output
match string // output is matched against this regex (when output is empty string)
}
type testCase struct {
give
want
}
func testCommands(t *testing.T, tests []testCase) {
// common test parameters
delim := "\t"
delimiter := Delimiter{str: &delim}
printsep := ""
stripAnsi := false
forcePlus := false
// evaluate the test cases
for idx, test := range tests {
gotOutput := replacePlaceholder(
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query,
test.give.allItems)
switch {
case test.want.output != "":
if gotOutput != test.want.output {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.output)
}
case test.want.match != "":
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
wantRegex := regexp.MustCompile(wantMatch)
if !wantRegex.MatchString(gotOutput) {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.match)
}
default:
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
}
}
}
// naive encoder of placeholder flags
func (flags placeholderFlags) encodePlaceholder() string {
encoded := ""
if flags.plus {
encoded += "+"
}
if flags.preserveSpace {
encoded += "s"
}
if flags.number {
encoded += "n"
}
if flags.file {
encoded += "f"
}
if flags.query {
encoded += "q"
}
return encoded
}
// can be replaced with os.ReadFile() in go 1.16+
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data := make([]byte, 0, 128)
for {
if len(data) >= cap(data) {
d := append(data[:cap(data)], 0)
data = d[:len(data)]
}
n, err := file.Read(data[len(data):cap(data)])
data = data[:len(data)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return data, err
}
}
}

View File

@@ -5,6 +5,7 @@ package fzf
import (
"os"
"os/signal"
"strings"
"syscall"
)
@@ -19,3 +20,7 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}
func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}

View File

@@ -4,6 +4,8 @@ package fzf
import (
"os"
"regexp"
"strings"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -17,3 +19,12 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}
func quoteEntry(entry string) string {
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
}

View File

@@ -73,6 +73,9 @@ func (r *LightRenderer) initPlatform() error {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
_, err := util.Read(fd, b)
if err == nil {
r.ttyinChannel <- b[0]

View File

@@ -120,8 +120,11 @@ func (a Attr) Merge(b Attr) Attr {
return a | b
}
// handle the following as private members of FullscreenRenderer instance
// they are declared here to prevent introducing tcell library in non-windows builds
var (
_screen tcell.Screen
_prevMouseButton tcell.ButtonMask
)
func (r *FullscreenRenderer) initScreen() {
@@ -185,14 +188,48 @@ func (r *FullscreenRenderer) GetChar() Event {
// process mouse events:
case *tcell.EventMouse:
// mouse down events have zeroed buttons, so we can't use them
// mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed 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
x, y := ev.Position()
button := ev.Buttons()
mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 {
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
prevButton, button := _prevMouseButton, ev.Buttons()
_prevMouseButton = button
drag := prevButton == button
switch {
case button&tcell.WheelDown != 0:
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
} else if button&tcell.WheelUp != 0 {
case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
} else if runtime.GOOS != "windows" {
case button&tcell.Button1 != 0 && !drag:
// all potential double click events put their 'line' coordinate in the clickY array
// double click event has two conditions, temporal and spatial, the first is checked here
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
}
r.prevDownTime = now
// detect double clicks (also check for spatial condition)
n := len(r.clickY)
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
if double {
// make sure two consecutive double clicks require four clicks
r.clickY = []int{}
}
// fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
case button&tcell.Button2 != 0 && !drag:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
case runtime.GOOS != "windows":
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
// to consume them.
@@ -223,9 +260,13 @@ func (r *FullscreenRenderer) GetChar() Event {
// process keyboard:
case *tcell.EventKey:
mods := ev.Modifiers()
none := mods == tcell.ModNone
alt := (mods & tcell.ModAlt) > 0
ctrl := (mods & tcell.ModCtrl) > 0
shift := (mods & tcell.ModShift) > 0
ctrlAlt := ctrl && alt
altShift := alt && shift
keyfn := func(r rune) Event {
if alt {
return CtrlAltKey(r)
@@ -233,6 +274,7 @@ func (r *FullscreenRenderer) GetChar() Event {
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
}
switch ev.Key() {
// section 1: Ctrl+(Alt)+[a-z]
case tcell.KeyCtrlA:
return keyfn('a')
case tcell.KeyCtrlB:
@@ -248,7 +290,21 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyCtrlG:
return keyfn('g')
case tcell.KeyCtrlH:
switch ev.Rune() {
case 0:
if ctrl {
return Event{BSpace, 0, nil}
}
case rune(tcell.KeyCtrlH):
switch {
case ctrl:
return keyfn('h')
case alt:
return Event{AltBS, 0, nil}
case none, shift:
return Event{BSpace, 0, nil}
}
}
case tcell.KeyCtrlI:
return keyfn('i')
case tcell.KeyCtrlJ:
@@ -285,20 +341,25 @@ func (r *FullscreenRenderer) GetChar() Event {
return keyfn('y')
case tcell.KeyCtrlZ:
return keyfn('z')
// section 2: Ctrl+[ \]_]
case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash:
return Event{CtrlBackSlash, 0, nil}
case tcell.KeyCtrlRightSq:
return Event{CtrlRightBracket, 0, nil}
case tcell.KeyCtrlCarat:
return Event{CtrlCaret, 0, nil}
case tcell.KeyCtrlUnderscore:
return Event{CtrlSlash, 0, nil}
// section 3: (Alt)+Backspace2
case tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
}
return Event{BSpace, 0, nil}
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp:
if altShift {
return Event{AltSUp, 0, nil}
@@ -344,6 +405,7 @@ func (r *FullscreenRenderer) GetChar() Event {
}
return Event{Right, 0, nil}
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
case tcell.KeyInsert:
return Event{Insert, 0, nil}
case tcell.KeyHome:
@@ -356,10 +418,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{PgUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
@@ -385,20 +445,31 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyF12:
return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space:
// section 6: (Ctrl+Alt)+'rune'
case tcell.KeyRune:
r := ev.Rune()
if alt {
return AltKey(r)
}
return Event{Rune, r, nil}
switch {
// translate native key events to ascii control characters
case r == ' ' && ctrl:
return Event{CtrlSpace, 0, nil}
// handle AltGr characters
case ctrlAlt:
return Event{Rune, r, nil} // dropping modifiers
// simple characters (possibly with modifier)
case alt:
return AltKey(r)
default:
return Event{Rune, r, nil}
}
// section 7: Esc
case tcell.KeyEsc:
return Event{ESC, 0, nil}
}
}
// section 8: Invalid
return Event{Invalid, 0, nil}
}

392
src/tui/tcell_test.go Normal file
View File

@@ -0,0 +1,392 @@
// +build tcell windows
package tui
import (
"testing"
"github.com/gdamore/tcell"
"github.com/junegunn/fzf/src/util"
)
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
if got == want {
return true
} else {
t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want)
return false
}
}
// Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) {
if util.ToTty() {
// This test is skipped when output goes to terminal, because it causes
// some glitches:
// - output lines may not start at the beginning of a row which makes
// the output unreadable
// - terminal may get cleared which prevents you from seeing results of
// previous tests
// Good ways to prevent the glitches are piping the output to a pager
// or redirecting to a file. I've found `less +G` to be trouble-free.
t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.")
} else if testing.Verbose() {
// I have observed a behaviour when this test outputted more than 8192
// bytes (32*256) into the 'less' pager, both the go's test executable
// and the pager hanged. The go's executable was blocking on printing.
// I was able to create minimal working example of that behaviour, but
// that example hanged after 12256 bytes (32*(256+127)).
t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.")
}
if !HasFullscreenRenderer() {
t.Skip("Can't test FullscreenRenderer.")
}
// construct test cases
type giveKey struct {
Type tcell.Key
Char rune
Mods tcell.ModMask
}
type wantKey = Event
type testCase struct {
giveKey
wantKey
}
/*
Some test cases are marked "fabricated". It means that giveKey value
is valid, but it is not what you get when you press the keys. For
example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX
(End-Of-Text character, causing SIGINT).
I was trying to accompany the fabricated test cases with real ones.
Some test cases are marked "unhandled". It means that giveKey.Type
is not present in tcell.go source code. It can still be handled via
implicit or explicit alias.
If not said otherwise, test cases are for US keyboard.
(tabstop=44)
*/
tests := []testCase{
// section 1: Ctrl+(Alt)+[a-z]
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}},
{giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated
{giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C)
{giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated
// KeyTab is alias for KeyTAB
{giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated
{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
// KeyEnter is alias for KeyCR
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
// 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 | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
// section 2: Ctrl+[ \]_]
{giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated
{giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' '
{giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}},
{giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}},
{giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated
{giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke
{giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}},
// section 3: (Alt)+Backspace2
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
// section 6: (Ctrl+Alt)+'rune'
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
{giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}},
{giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}},
{giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}},
/*
"Input method" in Windows Language options:
US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range
CS: "Czech keyboard"
DE: "German keyboard"
Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`.
*/
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level
{giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$"
{giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~"
{giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{"
{giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@"
{giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ"
// section 7: Esc
// KeyEsc and KeyEscape are aliases for KeyESC
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
// section 8: Invalid
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
{giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}},
{giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF
{giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
}
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
r.Init()
// run and evaluate the tests
for _, test := range tests {
// generate key event
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
_screen.PostEventWait(giveEvent)
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
// process the event in fzf and evaluate the test
gotEvent := r.GetChar()
// skip Resize events, those are sometimes put in the buffer outside of this test
for gotEvent.Type == Resize {
t.Logf("Resize swallowed")
gotEvent = r.GetChar()
}
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
}
r.Close()
}
/*
Quick reference
---------------
(tabstop=18)
(this is not mapping table, it merely puts multiple constants ranges in one table)
¹) the two columns are each other implicit alias
²) explicit aliases here
%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone
1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift
2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl
3 1 KeyCtrlC KeyETX = ^C CtrlC
4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt
5 1 KeyCtrlE KeyENQ = ^E CtrlE
6 1 KeyCtrlF KeyACK = ^F CtrlF
7 1 KeyCtrlG KeyBEL = ^G CtrlG
8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta
9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
11 1 KeyCtrlK KeyVT = ^K CtrlK
12 1 KeyCtrlL KeyFF = ^L CtrlL
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
14 1 KeyCtrlN KeySO = ^N CtrlN
15 1 KeyCtrlO KeySI = ^O CtrlO
16 1 KeyCtrlP KeyDLE = ^P CtrlP
17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ
18 1 KeyCtrlR KeyDC2 = ^R CtrlR
19 1 KeyCtrlS KeyDC3 = ^S CtrlS
20 1 KeyCtrlT KeyDC4 = ^T CtrlT
21 1 KeyCtrlU KeyNAK = ^U CtrlU
22 1 KeyCtrlV KeySYN = ^V CtrlV
23 1 KeyCtrlW KeyETB = ^W CtrlW
24 1 KeyCtrlX KeyCAN = ^X CtrlX
25 1 KeyCtrlY KeyEM = ^Y CtrlY
26 1 KeyCtrlZ KeySUB = ^Z CtrlZ
27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC
28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace
29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash
30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket
31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret
32 CtrlSlash
33 Invalid
34 Resize
35 Mouse
36 DoubleClick
37 LeftClick
38 RightClick
39 BTab
40 BSpace
41 Del
42 PgUp
43 PgDn
44 Up
45 Down
46 Left
47 Right
48 Home
49 End
50 Insert
51 SUp
52 SDown
53 SLeft
54 SRight
55 F1
56 F2
57 F3
58 F4
59 F5
60 F6
61 F7
62 F8
63 F9
64 F10
65 F11
66 F12
67 Change
68 BackwardEOF
69 AltBS
70 AltUp
71 AltDown
72 AltLeft
73 AltRight
74 AltSUp
75 AltSDown
76 AltSLeft
77 AltSRight
78 Alt
79 CtrlAlt
..
127 3 KeyDEL KeyBackspace2
..
256 6 KeyRune
257 4 KeyUp
258 4 KeyDown
259 4 KeyRight
260 4 KeyLeft
261 8 KeyUpLeft
262 8 KeyUpRight
263 8 KeyDownLeft
264 8 KeyDownRight
265 8 KeyCenter
266 5 KeyPgUp
267 5 KeyPgDn
268 5 KeyHome
269 5 KeyEnd
270 5 KeyInsert
271 5 KeyDelete
272 8 KeyHelp
273 8 KeyExit
274 8 KeyClear
275 8 KeyCancel
276 8 KeyPrint
277 8 KeyPause
278 5 KeyBacktab
279 5 KeyF1
280 5 KeyF2
281 5 KeyF3
282 5 KeyF4
283 5 KeyF5
284 5 KeyF6
285 5 KeyF7
286 5 KeyF8
287 5 KeyF9
288 5 KeyF10
289 5 KeyF11
290 5 KeyF12
291 8 KeyF13
292 8 KeyF14
293 8 KeyF15
294 8 KeyF16
295 8 KeyF17
296 8 KeyF18
297 8 KeyF19
298 8 KeyF20
299 8 KeyF21
300 8 KeyF22
301 8 KeyF23
302 8 KeyF24
303 8 KeyF25
304 8 KeyF26
305 8 KeyF27
306 8 KeyF28
307 8 KeyF29
308 8 KeyF30
309 8 KeyF31
310 8 KeyF32
311 8 KeyF33
312 8 KeyF34
313 8 KeyF35
314 8 KeyF36
315 8 KeyF37
316 8 KeyF38
317 8 KeyF39
318 8 KeyF40
319 8 KeyF41
320 8 KeyF42
321 8 KeyF43
322 8 KeyF44
323 8 KeyF45
324 8 KeyF46
325 8 KeyF47
326 8 KeyF48
327 8 KeyF49
328 8 KeyF50
329 8 KeyF51
330 8 KeyF52
331 8 KeyF53
332 8 KeyF54
333 8 KeyF55
334 8 KeyF56
335 8 KeyF57
336 8 KeyF58
337 8 KeyF59
338 8 KeyF60
339 8 KeyF61
340 8 KeyF62
341 8 KeyF63
342 8 KeyF64
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods
*/

View File

@@ -3,6 +3,7 @@ package util
import (
"math"
"os"
"strings"
"time"
"github.com/mattn/go-isatty"
@@ -21,7 +22,8 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop
} else {
w = runewidth.StringWidth(string(rs))
s := string(rs)
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
}
width += w
if limit > 0 && width > limit {
@@ -115,11 +117,16 @@ func DurWithin(
return val
}
// IsTty returns true is stdin is a terminal
// IsTty returns true if stdin is a terminal
func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd())
}
// ToTty returns true if stdout is a terminal
func ToTty() bool {
return isatty.IsTerminal(os.Stdout.Fd())
}
// Once returns a function that returns the specified boolean value only once
func Once(nextResponse bool) func() bool {
state := nextResponse

View File

@@ -2053,6 +2053,22 @@ class TestGoFZF < TestBase
tmux.send_keys 'dabcd'
tmux.until { |lines| assert_equal '> abcd', lines[-1] }
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.item_count
assert_includes lines[1], '[[0]]'
end
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
end
end
module TestShell

View File

@@ -104,10 +104,10 @@ if [ -d "${fish_dir}/functions" ]; then
remove "${fish_dir}/functions/fzf.fish"
remove "${fish_dir}/functions/fzf_key_bindings.fish"
if [ "$(ls -A "${fish_dir}/functions")" ]; then
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
else
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
rmdir "${fish_dir}/functions"
else
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
fi
fi