Compare commits

..

57 Commits

Author SHA1 Message Date
Junegunn Choi
dc975e8974 0.29.0 2021-12-25 01:46:01 +09:00
Junegunn Choi
4311ade535 ADVANCED.md: Add change-preview-window example 2021-12-24 14:41:47 +09:00
Junegunn Choi
cd23401411 Fix rendering of the prompt line when overflow occurs with --info=inline
Fix #2692
2021-12-22 23:23:50 +09:00
Martin Jindra
176ee6910f Update Dockerfile (#2662)
`archlinux/base:latest`cannot be found
2021-12-09 11:05:12 +09:00
Junegunn Choi
13c8f3d3aa [vim] Handle writefile() failure gracefully
Fix #2676
2021-12-07 17:33:26 +09:00
Junegunn Choi
ce9af687bc Remove unused code 2021-12-05 21:17:38 +09:00
Junegunn Choi
43f0d0cacd change-preview-window to take multiple option sets separated by '|'
So you can "rotate" through the different options with a single binding.

  fzf --preview 'cat {}' \
      --bind 'ctrl-/:change-preview-window(70%|down,40%,border-horizontal|hidden|)'

Close #2376
2021-12-05 21:13:10 +09:00
Junegunn Choi
20b4e6953e Implement change-preview and change-preview-window actions
The new actions are named with 'change-' prefix to differentiate from
the pre-existing, one-off 'preview(...)' action.

Fix #2360
Fix #2505
Fix #2666

Related #2435
Related #2376
  - Can set up multiple bindings with different change-preview-window actions
  - Not possible to "rotate" through the options with a single binding
  - Enlarge or shrink not possible
2021-11-30 23:57:46 +09:00
Kai
7da287e3aa README.md: HTTP => HTTPS (#2673) 2021-11-28 22:28:32 +09:00
zsugabubus
205f885d69 [shell] Use cd -- (#2659)
Otherwise directories starting with '-' may treated as options.
2021-11-19 10:36:28 +09:00
Junegunn Choi
3715cd349d Add repology packaging status badge 2021-11-18 15:47:06 +09:00
Junegunn Choi
e4c3ecc57e 0.28.0 2021-11-04 01:05:07 +09:00
Junegunn Choi
673c5d886d Add 'put' action for putting the character to the prompt
fzf --bind 'space:preview(date)+put'

Close #2456
2021-11-04 00:49:05 +09:00
Junegunn Choi
f799b568d1 [bash] Suppress error message from 'bind'
Fix #2618
2021-11-03 23:26:25 +09:00
Junegunn Choi
7bff4661f6 Add --header-first option to display header before prompt line
Close #2422
2021-11-03 21:19:22 +09:00
Junegunn Choi
ffd8bef808 Update CHANGELOG 2021-11-02 21:48:19 +09:00
Junegunn Choi
02cee2234d Implement --scroll-off=LINES
Close #2533
2021-11-02 21:48:19 +09:00
Vlastimil Ovčáčík
e0dd2be3fb Document escaping and expanding of quotes on Windows
Parsers included:
- go parser (well, this is easily dealt with using `` strings)
- win32 (shell-api) parser
- powershell parser (for powershell commands)
- powershell parsing rules for calling native commands
- internal parsers of select regex applications (like grep)
2021-11-02 15:56:20 +09:00
Vlastimil Ovčáčík
a33c011c21 Test escaping of powershell commands on Windows 2021-11-02 15:56:20 +09:00
Rashil Gandhi
7c3f42bbba Fix powershell escaping 2021-11-02 15:56:20 +09:00
Junegunn Choi
edac9820b5 Cache cygpath result
No need to repeatedly run cygpath process because $SHELL never changes.
2021-10-25 18:46:59 +09:00
Rashil Gandhi
84a47f7102 Respect SHELL env var on Windows (#2641)
This makes fzf respect SHELL environment variable on Windows, like it does on *nix, whenever defined.

Close #2638
2021-10-23 01:09:47 +09:00
Junegunn Choi
97ae8afb6f Reload should update preview window
Fix #2644
2021-10-23 01:06:15 +09:00
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
43 changed files with 1998 additions and 499 deletions

View File

@@ -17,6 +17,6 @@
- [ ] bash
- [ ] zsh
- [ ] fish
## Problem / Steps to reproduce

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

@@ -149,7 +149,7 @@ fzf-tmux -p 80%,60%
> You might also want to check out my tmux plugins which support this popup
> window layout.
>
>
> - https://github.com/junegunn/tmux-fzf-url
> - https://github.com/junegunn/tmux-fzf-maccy
@@ -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
@@ -429,30 +429,34 @@ Admittedly, that was a silly example. Here's a practical one for browsing
Kubernetes pods.
```bash
#!/usr/bin/env bash
read -ra tokens < <(
kubectl get pods --all-namespaces |
fzf --info=inline --layout=reverse --header-lines=1 --border \
pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
fzf --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $'Press CTRL-O to open log in editor\n\n' \
--bind ctrl-/:toggle-preview \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --namespace {1} {2}) > /dev/tty' \
--preview-window up,follow \
--preview 'kubectl logs --follow --tail=100000 --namespace {1} {2}' "$@"
)
[ ${#tokens} -gt 1 ] &&
kubectl exec -it --namespace "${tokens[0]}" "${tokens[1]}" -- bash
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}
```
![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png)
- The preview window will *"log tail"* the pod
- Holding on to a large amount of log will consume a lot of memory. So we
limited the initial log amount with `--tail=100000`.
- With `execute` binding, you can press CTRL-O to open the log in your editor
without leaving fzf
- Select a pod (with an enter key) to `kubectl exec` into it
limited the initial log amount with `--tail=10000`.
- `execute` bindings allow you to run any command without leaving fzf
- Press enter key on a pod to `kubectl exec` into it
- Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
window options
1. `80%,border-bottom`
1. `hidden`
1. Empty string after `|` translates to the default options from `--preview-window`
Key bindings for git objects
----------------------------

View File

@@ -1,6 +1,57 @@
CHANGELOG
=========
0.29.0
------
- Added `change-preview(...)` action to change the `--preview` command
- cf. `preview(...)` is a one-off action that doesn't change the default
preview command
- Added `change-preview-window(...)` action
- You can rotate through the different options separated by `|`
```sh
fzf --preview 'cat {}' --preview-window right:40% \
--bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-top|hidden|)'
```
- Fixed rendering of the prompt line when overflow occurs with `--info=inline`
0.28.0
------
- Added `--header-first` option to print header before the prompt line
```sh
fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first
```
- Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim)
- You can set it to a very large number so that the cursor stays in the
middle of the screen while scrolling
```sh
fzf --scroll-off=5
fzf --scroll-off=999
```
- Fixed bug where preview window is not updated on `reload` (#2644)
- fzf on Windows will also use `$SHELL` to execute external programs
- See #2638 and #2647
- Thanks to @rashil2000, @vovcacik, and @janlazo
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
@@ -21,7 +72,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 } }
```
@@ -71,7 +122,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
@@ -1153,4 +1204,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

@@ -1,4 +1,4 @@
FROM archlinux/base:latest
FROM archlinux
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc

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
@@ -99,6 +102,7 @@ endif
grep -qF $(VERSION) install.ps1
# Make release note out of CHANGELOG.md
mkdir -p tmp
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
@@ -141,6 +145,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

@@ -86,7 +86,7 @@ stuff.
### Using Homebrew
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
You can use [Homebrew](https://brew.sh/) (on macOS or Linux)
to install fzf.
```sh
@@ -131,6 +131,8 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
>
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`)
[![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions)
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
@@ -202,7 +204,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
@@ -574,7 +576,7 @@ FZF_DEFAULT_COMMAND='find . -type f' \
The following example uses fzf as the selector interface for ripgrep. We bound
`reload` action to `change` event, so every time you type on fzf, the ripgrep
process will restart with the updated query string denoted by the placeholder
expression `{q}`. Also, note that we used `--disabled` option so that fzf
expression `{q}`. Also, note that we used `--disabled` option so that fzf
doesn't perform any secondary filtering.
```sh

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.2
version=0.29.0
auto_completion=
key_bindings=
update_config=2

View File

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

View File

@@ -1,11 +1,11 @@
package main
import (
"github.com/junegunn/fzf/src"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.27"
var version string = "0.29"
var revision string = "devel"
func main() {

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 "Jun 2021" "fzf 0.27.2" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Dec 2021" "fzf 0.29.0" "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 "Jun 2021" "fzf 0.27.2" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -135,10 +135,14 @@ Enable cyclic scroll
Keep the right end of the line visible when it's too long. Effective only when
the query string is empty.
.TP
.BI "--scroll-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to
the bottom (default: 0).
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
.BI "--hscroll-off=" "COLS"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
@@ -295,6 +299,9 @@ are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.TP
.B "--header-first"
Print header before the prompt line
.SS Display
.TP
.B "--ansi"
@@ -803,76 +810,79 @@ e.g.
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR
\fBpreview-bottom\fR
\fBpreview-top\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\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)
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\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-preview\fR
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
@@ -962,7 +972,6 @@ commands in addition to the default preview command given by \fB--preview\fR
option.
e.g.
# Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}'
@@ -973,6 +982,22 @@ e.g.
# Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden
.SS CHANGE PREVIEW WINDOW ATTRIBUTES
\fBchange-preview-window\fR action can be used to change the properties of the
preview window. Unlike the \fB--preview-window\fR option, you can specify
multiple sets of options separated by '|' characters.
e.g.
# Rotate through the options using CTRL-/
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)'
# The default properties given by `--preview-window` are inherited, so an empty string in the list is interpreted as the default
fzf --preview 'cat {}' --preview-window 'right,40%,border-left' --bind 'ctrl-/:change-preview-window(70%|down,border-top|hidden|)'
# This is equivalent to toggle-preview action
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(hidden|)'
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

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
@@ -444,6 +444,12 @@ function! s:use_sh()
return [shell, shellslash, shellcmdflag, shellxquote]
endfunction
function! s:writefile(...)
if call('writefile', a:000) == -1
throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.'
endif
endfunction
function! fzf#run(...) abort
try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -471,7 +477,7 @@ try
let source_command = source
elseif type == 3
let temps.input = s:fzf_tempname()
call writefile(source, temps.input)
call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
else
throw 'Invalid source type'
@@ -515,11 +521,12 @@ try
call s:callback(dict, lines)
return lines
finally
if len(source_command)
if exists('source_command') && len(source_command)
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]
@@ -659,7 +666,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif
if s:is_win
let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile)
call s:writefile(s:wrap_cmds(command), batchfile)
let command = batchfile
let a:temps.batchfile = batchfile
if has('nvim')
@@ -677,7 +684,7 @@ function! s:execute(dict, command, use_height, temps) abort
endif
elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname()
call writefile([command], shellscript)
call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript
endif
@@ -876,7 +883,7 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(a:dict)
if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile
else
let command = a:command
@@ -943,6 +950,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

@@ -32,12 +32,12 @@ fi
###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
bind '"\e[0n": redraw-current-line' 2> /dev/null
__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"
}
@@ -41,7 +41,7 @@ __fzf_cd__() {
local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd -- %q' "$dir"
}
__fzf_history__() {
@@ -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

@@ -87,7 +87,7 @@ function fzf_key_bindings
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ]
cd $result
cd -- $result
# Remove last token from commandline.
commandline -t ""

View File

@@ -79,7 +79,7 @@ fzf-cd-widget() {
return 0
fi
zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="cd ${(q)dir}"
BUFFER="cd -- ${(q)dir}"
zle accept-line
local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion

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

@@ -44,8 +44,10 @@ const usage = `usage: fzf [options]
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll
--keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0)
--no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the
--hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept
@@ -67,6 +69,7 @@ const usage = `usage: fzf [options]
--marker=STR Multi-select marker (default: '>')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line
Display
--ansi Enable processing of ANSI color codes
@@ -173,6 +176,14 @@ type previewOpts struct {
headerLines int
}
func (a previewOpts) sameLayout(b previewOpts) bool {
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden
}
func (a previewOpts) sameContentLayout(b previewOpts) bool {
return a.wrap == b.wrap && a.headerLines == b.headerLines
}
// Options stores the values of command-line options
type Options struct {
Fuzzy bool
@@ -200,6 +211,7 @@ type Options struct {
KeepRight bool
Hscroll bool
HscrollOff int
ScrollOff int
FileWord bool
InfoStyle infoStyle
JumpLabels string
@@ -212,7 +224,7 @@ type Options struct {
Filter *string
ToggleSort bool
Expect map[tui.Event]string
Keymap map[tui.Event][]action
Keymap map[tui.Event][]*action
Preview previewOpts
PrintQuery bool
ReadZero bool
@@ -222,6 +234,7 @@ type Options struct {
History *History
Header []string
HeaderLines int
HeaderFirst bool
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
@@ -261,6 +274,7 @@ func defaultOptions() *Options {
KeepRight: false,
Hscroll: true,
HscrollOff: 10,
ScrollOff: 0,
FileWord: false,
InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
@@ -273,7 +287,7 @@ func defaultOptions() *Options {
Filter: nil,
ToggleSort: false,
Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]action),
Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""),
PrintQuery: false,
ReadZero: false,
@@ -283,6 +297,7 @@ func defaultOptions() *Options {
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
HeaderFirst: false,
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
@@ -780,10 +795,10 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
}
func parseKeymap(keymap map[tui.Event][]action, str string) {
func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":"
if strings.HasPrefix(src, "+") {
@@ -792,6 +807,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "change-preview-window") {
prefix = symbol + "change-preview-window"
} else if strings.HasPrefix(src[1:], "change-preview") {
prefix = symbol + "change-preview"
} else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") {
@@ -835,7 +854,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+")
actions := make([]action, 0, len(specs))
actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...)
}
@@ -974,6 +993,12 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actEnableSearch)
case "disable-search":
appendAction(actDisableSearch)
case "put":
if key.Type == tui.Rune && unicode.IsGraphic(key.Char) {
appendAction(actRune)
} else {
errorExit("unable to put non-printable character: " + pair[0])
}
default:
t := isExecuteAction(specLower)
if t == actIgnore {
@@ -989,6 +1014,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("reload")
case actPreview:
offset = len("preview")
case actChangePreviewWindow:
offset = len("change-preview-window")
case actChangePreview:
offset = len("change-preview")
case actChangePrompt:
offset = len("change-prompt")
case actUnbind:
@@ -1004,17 +1033,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg})
actions = append(actions, &action{t: t, a: actionArg})
} else {
prevSpec = spec + "+"
continue
}
} else {
actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, action{t: t, a: actionArg})
actions = append(actions, &action{t: t, a: actionArg})
}
if t == actUnbind {
parseKeyChords(actionArg, "unbind target required")
} else if t == actChangePreviewWindow {
opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") {
parsePreviewWindow(&opts, arg)
}
}
}
}
@@ -1040,6 +1074,10 @@ func isExecuteAction(str string) actionType {
return actUnbind
case "preview":
return actPreview
case "change-preview-window":
return actChangePreviewWindow
case "change-preview":
return actChangePreview
case "change-prompt":
return actChangePrompt
case "execute":
@@ -1052,7 +1090,7 @@ func isExecuteAction(str string) actionType {
return actIgnore
}
func parseToggleSort(keymap map[tui.Event][]action, str string) {
func parseToggleSort(keymap map[tui.Event][]*action, str string) {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
errorExit("multiple keys specified")
@@ -1354,6 +1392,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = false
case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--scroll-off":
opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
@@ -1421,6 +1461,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines":
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--header-first":
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
@@ -1530,6 +1574,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
opts.ScrollOff = atoi(value)
} else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value
validateJumpLabels = true
@@ -1547,6 +1593,10 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("hscroll offset must be a non-negative integer")
}
if opts.ScrollOff < 0 {
errorExit("scroll offset must be a non-negative integer")
}
if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer")
}
@@ -1592,7 +1642,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
@@ -1608,11 +1658,29 @@ func postProcessOptions(opts *Options) {
// Extend the default key map
keymap := defaultKeymap()
for key, actions := range opts.Keymap {
var lastChangePreviewWindow *action
for _, act := range actions {
if act.t == actToggleSort {
switch act.t {
case actToggleSort:
// To display "+S"/"-S" on info line
opts.ToggleSort = true
case actChangePreviewWindow:
lastChangePreviewWindow = act
}
}
// Re-organize actions so that we only keep the last change-preview-window
// and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow != nil {
reordered := []*action{lastChangePreviewWindow}
for _, act := range actions {
if act.t != actChangePreviewWindow {
reordered = append(reordered, act)
}
}
actions = reordered
}
keymap[key] = actions
}
opts.Keymap = keymap

View File

@@ -22,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Normal command
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs)
}

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"io/ioutil"
"math"
"os"
"os/signal"
"regexp"
@@ -23,6 +24,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
@@ -84,85 +105,89 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
spinner []string
prompt func()
promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]action
pressed string
printQuery bool
history *History
cycle bool
header []string
header0 []string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
previewOpts previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
initDelay time.Duration
infoStyle infoStyle
spinner []string
prompt func()
promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
pressed string
printQuery bool
history *History
cycle bool
headerFirst bool
headerLines int
header []string
header0 []string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
}
type selectedItem struct {
@@ -193,6 +218,7 @@ const (
reqRefresh
reqReinit
reqRedraw
reqFullRedraw
reqClose
reqPrintQuery
reqPreviewEnqueue
@@ -263,6 +289,8 @@ const (
actTogglePreview
actTogglePreviewWrap
actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
@@ -301,9 +329,10 @@ type searchRequest struct {
}
type previewRequest struct {
template string
pwindow tui.Window
list []*Item
template string
pwindow tui.Window
scrollOffset int
list []*Item
}
type previewResult struct {
@@ -313,16 +342,16 @@ type previewResult struct {
spinner string
}
func toActions(types ...actionType) []action {
actions := make([]action, len(types))
func toActions(types ...actionType) []*action {
actions := make([]*action, len(types))
for idx, t := range types {
actions[idx] = action{t: t, a: ""}
actions[idx] = &action{t: t, a: ""}
}
return actions
}
func defaultKeymap() map[tui.Event][]action {
keymap := make(map[tui.Event][]action)
func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a)
}
@@ -393,7 +422,7 @@ func trimQuery(query string) []rune {
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
if action.t == actPreview {
if action.t == actPreview || action.t == actChangePreview {
return true
}
}
@@ -425,6 +454,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()
}
@@ -472,69 +502,73 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
header: header,
header0: header,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
initDelay: delay,
infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: header,
header0: header,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -676,7 +710,7 @@ func (t *Terminal) sortSelected() []selectedItem {
}
func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, 0)
width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width
}
@@ -953,12 +987,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
return before, after
}
func (t *Terminal) promptLine() int {
if t.headerFirst {
max := t.window.Height() - 1
if !t.noInfoLine() {
max--
}
return util.Min(len(t.header0)+t.headerLines, max)
}
return 0
}
func (t *Terminal) placeCursor() {
t.move(0, t.promptLen+t.queryLen[0], false)
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
}
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
t.move(t.promptLine(), 0, true)
t.prompt()
before, after := t.updatePromptOffset()
@@ -980,22 +1025,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
func (t *Terminal) printInfo() {
pos := 0
line := t.promptLine()
switch t.infoStyle {
case infoDefault:
t.move(1, 0, true)
t.move(line+1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
}
t.move(1, 2, false)
t.move(line+1, 2, false)
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() {
return
}
t.move(0, pos, true)
t.move(line, pos, true)
if t.reading {
t.window.CPrint(tui.ColSpinner, " < ")
} else {
@@ -1038,11 +1084,20 @@ func (t *Terminal) printHeader() {
return
}
max := t.window.Height()
if t.headerFirst {
max--
if !t.noInfoLine() {
max--
}
}
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
if t.noInfoLine() {
line--
line := idx
if !t.headerFirst {
line++
if !t.noInfoLine() {
line++
}
}
if line >= max {
continue
@@ -1143,7 +1198,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters
width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 {
return runes[:overflowIdx], true
}
@@ -1519,22 +1574,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 +1599,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
skipChars++
case 'q':
flags.query = true
// query flag is not skipped
default:
break
}
@@ -1609,9 +1649,14 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
@@ -1622,20 +1667,21 @@ func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
}
base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
if denom == 0 {
return base
if denom != 0 {
base -= height / denom
}
return base - height/denom
break
}
base += atoi(component)
}
return base
return util.Max(0, base)
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1647,77 +1693,86 @@ 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
case match == "{q}":
return quoteEntry(query)
case match == "{}":
replace = func(item *Item) string {
switch {
case flags.number:
n := int(item.text.Index)
if n < 0 {
return ""
}
return strconv.Itoa(n)
case flags.file:
return item.AsString(stripAnsi)
default:
return quoteEntry(item.AsString(stripAnsi))
}
}
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
}
ranges[idx] = r
}
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)
}
return str
}
}
// Current query
if match == "{q}" {
return quoteEntry(query)
}
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || forcePlus {
items = selected
}
replacements := make([]string, len(items))
if match == "{}" {
for idx, item := range items {
if flags.number {
n := int(item.text.Index)
if n < 0 {
replacements[idx] = ""
} else {
replacements[idx] = strconv.Itoa(n)
}
} else if flags.file {
replacements[idx] = item.AsString(stripAnsi)
} else {
replacements[idx] = 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)
if !ok {
// Invalid expression, just return the original string in the template
return match
}
ranges[idx] = r
}
for idx, item := range items {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := joinTokens(trans)
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1)
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
replacements[idx] = replace(item)
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
@@ -1725,8 +1780,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
})
}
func (t *Terminal) redraw() {
t.tui.Clear()
func (t *Terminal) redraw(clear bool) {
if clear {
t.tui.Clear()
}
t.tui.Refresh()
t.printAll()
}
@@ -1746,7 +1803,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.tui.Pause(true)
cmd.Run()
t.tui.Resume(true, false)
t.redraw()
t.redraw(true)
t.refresh()
} else {
t.tui.Pause(false)
@@ -1891,7 +1948,7 @@ func (t *Terminal) Loop() {
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
t.reqBox.Set(reqFullRedraw, nil)
}
}()
@@ -1930,12 +1987,14 @@ func (t *Terminal) Loop() {
var items []*Item
var commandTemplate string
var pwindow tui.Window
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list
pwindow = request.pwindow
}
@@ -1947,11 +2006,9 @@ func (t *Terminal) Loop() {
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
initialOffset := 0
cmd := util.ExecCommand(command, true)
if pwindow != nil {
height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -2086,7 +2143,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list})
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
@@ -2141,9 +2198,11 @@ func (t *Terminal) Loop() {
t.suppress = false
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
t.redraw(true)
case reqRedraw:
t.redraw()
t.redraw(false)
case reqFullRedraw:
t.redraw(true)
case reqClose:
exit(func() int {
if t.output() {
@@ -2211,13 +2270,15 @@ func (t *Terminal) Loop() {
}
}
}
togglePreview := func(enabled bool) {
togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled {
t.previewer.enabled = enabled
t.tui.Clear()
// We need to immediately update t.pwindow so we don't use reqRedraw
t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader)
return true
}
return false
}
toggle := func() bool {
current := t.currentItem()
@@ -2254,12 +2315,12 @@ func (t *Terminal) Loop() {
}
}
actionsFor := func(eventType tui.EventType) []action {
actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()]
}
var doAction func(action) bool
doActions := func(actions []action) bool {
var doAction func(*action) bool
doActions := func(actions []*action) bool {
for _, action := range actions {
if !doAction(action) {
return false
@@ -2267,7 +2328,7 @@ func (t *Terminal) Loop() {
}
return true
}
doAction = func(a action) bool {
doAction = func(a *action) bool {
switch a.t {
case actIgnore:
case actExecute, actExecuteSilent:
@@ -2285,7 +2346,7 @@ func (t *Terminal) Loop() {
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, list})
previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
}
@@ -2447,14 +2508,14 @@ func (t *Terminal) Loop() {
}
case actToggleIn:
if t.layout != layoutDefault {
return doAction(action{t: actToggleUp})
return doAction(&action{t: actToggleUp})
}
return doAction(action{t: actToggleDown})
return doAction(&action{t: actToggleDown})
case actToggleOut:
if t.layout != layoutDefault {
return doAction(action{t: actToggleDown})
return doAction(&action{t: actToggleDown})
}
return doAction(action{t: actToggleUp})
return doAction(&action{t: actToggleUp})
case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
@@ -2478,7 +2539,7 @@ func (t *Terminal) Loop() {
req(reqClose)
}
case actClearScreen:
req(reqRedraw)
req(reqFullRedraw)
case actClearQuery:
t.input = []rune{}
t.cx = 0
@@ -2627,7 +2688,7 @@ func (t *Terminal) Loop() {
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
if my == t.promptLine() && mx >= 0 {
// Prompt
t.cx = mx + t.xoffset
} else if my >= min {
@@ -2657,12 +2718,53 @@ func (t *Terminal) Loop() {
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
t.reading = true
t.version++
}
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
delete(t.keymap, key)
}
case actChangePreview:
if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0)
t.previewOpts.command = a.a
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
// Split window options
tokens := strings.Split(a.a, "|")
parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
}
if t.previewOpts.hidden {
togglePreview(false)
} else {
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
if togglePreview(true) {
refreshPreview(t.previewOpts.command)
} else {
req(reqRedraw)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll {
scrollPreviewTo(t.evaluateScrollOffset())
}
}
}
return true
}
@@ -2670,7 +2772,7 @@ func (t *Terminal) Loop() {
if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune {
doAction(action{t: actRune})
doAction(&action{t: actRune})
} else if !doActions(actions) {
continue
}
@@ -2732,9 +2834,26 @@ func (t *Terminal) constrain() {
t.cy = util.Constrain(t.cy, 0, count-1)
minOffset := t.cy - height + 1
minOffset := util.Max(t.cy-height+1, 0)
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
t.offset = util.Constrain(t.offset, minOffset, maxOffset)
if t.scrollOff == 0 {
return
}
scrollOff := util.Min(height/2, t.scrollOff)
for {
prevOffset := t.offset
if t.cy-t.offset < scrollOff {
t.offset = util.Max(minOffset, t.offset-1)
}
if t.cy-t.offset >= height-scrollOff {
t.offset = util.Min(maxOffset, t.offset+1)
}
if t.offset == prevOffset {
break
}
}
}
func (t *Terminal) vmove(o int, allowCycle bool) {

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,513 @@ 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 TestQuoteEntryCmd(t *testing.T) {
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
}
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
// (not necessarily unexpected)
// 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
// (not necessarily unexpected)
// 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"`
// TODO this should actually work
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
}
testCommands(t, tests)
}
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
func TestPowershellCommands(t *testing.T) {
if !util.IsWindows() {
t.SkipNow()
}
tests := []testCase{
// reference: give{template, query, items}, want{output OR match}
/*
You can read each line in the following table as a pipeline that
consist of series of parsers that act upon your input (col. 1) and
each cell represents the output value.
For example:
- exec.Command("program.exe", `\''`)
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
" empty string² missing argument error ... ... |
\" literal " unbalanced quote error ... ... |
'\"' literal '"' literal " empty string empty string (match all) | yes
'\\\"' literal '\"' literal \" literal " literal " |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
\ transparent transparent transparent regex error |
'\' transparent literal \ literal \ regex error | yes
\\ transparent transparent transparent literal \ |
'\\' transparent literal \\ literal \\ literal \ |
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
' transparent unbalanced quote error ... ... |
\' transparent literal \ and unb. quote error ... ... |
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
''' transparent unbalanced quote error ... ... |
'''' transparent literal ' literal ' literal ' | yes
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
¹: native commands like grep, git grep, ripgrep
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
*/
// 1) working examples
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
// example of escaping single quotes
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
// chaining powershells
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
// 2) problematic examples
// (not necessarily unexpected)
// looking for a path string will only work with escaped backslashes
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
// looking for a literal double quote will only work with triple escaped double quotes
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
// to explorer, which will prompt user to pick editing program for the fzf-preview file
// the temp file contains: `cat "C:\test.txt"`
// TODO this should actually work
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
}
// to force powershell-style escaping we temporarily set environment variable that fzf honors
shellBackup := os.Getenv("SHELL")
os.Setenv("SHELL", "powershell")
testCommands(t, tests)
os.Setenv("SHELL", shellBackup)
}
/*
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,27 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}
func quoteEntry(entry string) string {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
if strings.Contains(shell, "cmd") {
// backslash escaping is done here for applications
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
// caret is the escape character for cmd shell
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}

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
_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:
return keyfn('h')
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

@@ -26,7 +26,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
}
width += w
if limit > 0 && width > limit {
if width > limit {
return width, idx
}
idx += len(rs)
@@ -117,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

@@ -38,3 +38,19 @@ func TestOnce(t *testing.T) {
t.Error("Expected: false")
}
}
func TestRunesWidth(t *testing.T) {
for _, args := range [][]int{
{100, 5, -1},
{3, 4, 3},
{0, 1, 0},
} {
width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0])
if width != args[1] {
t.Errorf("Expected width: %d, actual: %d", args[1], width)
}
if overflowIdx != args[2] {
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
}
}
}

View File

@@ -6,23 +6,57 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"sync/atomic"
"syscall"
)
// ExecCommand executes the given command with cmd
var shellPath atomic.Value
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd {
return ExecCommandWith("cmd", command, setpgid)
var shell string
if cached := shellPath.Load(); cached != nil {
shell = cached.(string)
} else {
shell = os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
shell = strings.Trim(string(out), "\n")
}
}
shellPath.Store(shell)
}
return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with cmd. _shell parameter is
// ignored on Windows.
// ExecCommandWith executes the given command with the specified shell
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command("cmd")
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
var cmd *exec.Cmd
if strings.Contains(shell, "cmd") {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CreationFlags: 0,
}
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else {
cmd = exec.Command(shell, "-c", command)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CreationFlags: 0,
}
return cmd

View File

@@ -2053,6 +2053,161 @@ 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
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_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
height = tmux.until { |lines| lines }.first.to_i
tmux.send_keys :PgUp
tmux.until do |lines|
assert_equal height + 3, lines.first.to_i
assert_equal "> #{height}", lines[3].strip
end
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 1}", lines[3].strip }
tmux.send_keys 'l'
tmux.until { |lines| assert_equal '> 1000', lines.first.strip }
tmux.send_keys :PgDn
tmux.until { |lines| assert_equal "> #{1000 - height + 1}", lines.reverse[5].strip }
tmux.send_keys :Down
tmux.until { |lines| assert_equal "> #{1000 - height}", lines.reverse[5].strip }
end
def test_scroll_off_large
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=9999", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count }
height = tmux.until { |lines| lines }.first.to_i
tmux.send_keys :PgUp
tmux.until { |lines| assert_equal "> #{height}", lines[height / 2].strip }
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
tmux.send_keys :Up
tmux.until { |lines| assert_equal "> #{height + 2}", lines[height / 2].strip }
tmux.send_keys :Down
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
end
def test_header_first
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
tmux.until do |lines|
expected = <<~OUTPUT
> 4
997/997
>
3
2
1
foobar
OUTPUT
assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n")
end
end
def test_header_first_reverse
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
tmux.until do |lines|
expected = <<~OUTPUT
foobar
1
2
3
> < 997/997
> 4
OUTPUT
assert_equal expected.chomp, lines.take(6).join("\n")
end
end
def test_change_preview_window
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --preview-window border-none --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.item_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_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
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
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