Compare commits

...

96 Commits

Author SHA1 Message Date
Junegunn Choi
0818dbc36a 0.27.1 2021-05-22 13:19:57 +09:00
Junegunn Choi
347c4b2625 Add 'unbind' action
Fix #2486
2021-05-22 13:16:39 +09:00
Junegunn Choi
34f0d4d0c4 [man] Clarification on --select-1 and --exit-0 2021-05-22 09:47:02 +09:00
Junegunn Choi
cbedb57511 [vim] Workaround for Neovim bug of unconditionally evaluating unlet $ENV_VAR
See #2495
2021-05-21 13:17:19 +09:00
Junegunn Choi
9ef825d2fd [vim] Update README-VIM 2021-05-19 18:43:50 +09:00
Junegunn Choi
85ae745910 [vim] Use terminal buffer on 'down' layout on regular Vim on terminal
When 'down' layout was used on regular Vim on terminal, fzf would open
below the editor using `--height` option. This was the only case where
terminal buffer was not used (the code was written when Vim didn't have
builtin terminal) and this exception has been a constant source of
confusion.

This commit makes fzf open in a terminal buffer even in that case.
2021-05-19 18:39:12 +09:00
Junegunn Choi
7411da8d5a [vim] Use FZF_DEFAULT_COMMAND instead of STDIN pipe
So that fzf can finish immediately even when the input process doesn't
handle SIGPIPE and keeps running.

Fix #2481
2021-05-19 18:37:13 +09:00
Junegunn Choi
3f75a8369f Replace RuneWidth to StringWidth to handle grapheme clusters
Fix #2482
2021-05-14 11:44:44 +09:00
Junegunn Choi
4cd621e877 ADVANCED.md: tmux 3.2 is officially released 2021-04-29 09:37:17 +09:00
Junegunn Choi
6e3a2fe0bf [vim] Fix screen offset of relatively positioned popup window
Fix #2461
2021-04-28 10:17:46 +09:00
Tom Picton
8b0e1f941a [vim] Support relative-to-window positioning of popup (#2443)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-04-17 20:48:10 +09:00
Junegunn Choi
c7c5e7670a Fix goreleaser.yml 2021-04-10 14:19:19 +09:00
Junegunn Choi
f6c621ef1b Update ADVANCED.md
Remove unnecessary --color option
2021-04-09 22:58:31 +09:00
Junegunn Choi
faf32d451d Update ADVANCED.md 2021-04-09 14:14:53 +09:00
Junegunn Choi
252fd7ecb1 Update ADVANCED.md 2021-04-09 13:43:16 +09:00
Junegunn Choi
7fa89dddb4 Update README.md: Examples page 2021-04-08 10:06:26 +09:00
Junegunn Choi
fefdb8c84e Fix typo 2021-04-07 13:28:26 +09:00
Junegunn Choi
a6cc05936e ADVANCED.md: Clarification on {q} 2021-04-07 07:12:39 +09:00
Junegunn Choi
b209843545 Advanced fzf examples 2021-04-07 06:46:17 +09:00
Junegunn Choi
19759ed36e 0.27.0 2021-04-06 22:53:59 +09:00
Junegunn Choi
1a7ae8e7b9 Update dependencies
go get: upgraded github.com/lucasb-eyer/go-colorful v1.0.3 => v1.2.0
go get: upgraded github.com/mattn/go-runewidth v0.0.9 => v0.0.12
go get: upgraded github.com/mattn/go-shellwords v1.0.10 => v1.0.11
go get: added github.com/rivo/uniseg v0.2.0
go get: upgraded github.com/saracen/walker v0.1.1 => v0.1.2
go get: upgraded golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 => v0.0.0-20210220032951-036812b2e83c
go get: upgraded golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 => v0.0.0-20210403161142-5e06dd20ab57
go get: upgraded golang.org/x/text v0.3.3 => v0.3.6
2021-04-06 20:32:18 +09:00
Junegunn Choi
da1f645670 Change --preview-window delimiter from : to , for consistency
Delimiter : was chosen when --preview-option only supported position and
size attributes. e.g. up:50%
2021-04-06 20:10:55 +09:00
Junegunn Choi
3a2015ee26 Fix minimum preview window height 2021-04-06 20:05:54 +09:00
Junegunn Choi
c440418ce6 Sign and notarize macOS binaries
Close #2408
2021-04-06 18:09:06 +09:00
Junegunn Choi
3d37a5ba1d Apply preview-bg color to preview border of all shapes 2021-04-06 18:01:29 +09:00
Junegunn Choi
15f4cfb6d9 More border optins for preview window
Close #2431
2021-04-06 17:37:11 +09:00
Junegunn Choi
be36de2482 Ignore more ANSI escape sequences
Fix #2420
2021-04-06 00:51:39 +09:00
Junegunn Choi
391237f7df [vim] Compare binary versions
Close #2410
2021-04-06 00:24:20 +09:00
Junegunn Choi
977e5effd9 [vim] Fix paste on MacVim
Close https://github.com/junegunn/fzf.vim/issues/1233
2021-04-05 17:28:18 +09:00
Junegunn Choi
8b36a4cb19 Speed up preview switching when doing partial rendering
Fix #2417
2021-04-04 13:43:16 +09:00
Michael Kelley
c8cd94a772 Ensure proper ESC seq handling under Windows preview mode (#2430)
- Increase go routine buffer size
- Add time wait for nonblock getchr()
- Resolve #2429
2021-04-04 13:19:43 +09:00
Junegunn Choi
764316a53d Fix flaky test case: test_interrupt_execute
Try to avoid extraneous INT signal
2021-03-26 17:40:12 +09:00
Philipp Schmitt
2048fd4042 Update README (--phony -> --disabled) (#2404) 2021-03-25 20:36:01 +09:00
Junegunn Choi
f84b3de24b Automatically set /dev/tty as STDIN on execute action
https://github.com/junegunn/fzf/issues/1360#issuecomment-788178140

  # Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal"
  ls | fzf --bind "enter:execute(vim {} < /dev/tty)"

  # With this change, we can omit "< /dev/tty" part
  ls | fzf --bind "enter:execute(vim {})"
2021-03-25 20:00:09 +09:00
Junegunn Choi
6a1f3ec08b [install] Download Darwin arm64 binary (#2400 #2401) 2021-03-25 10:56:21 +09:00
Mitsuo Heijo
2e353aee96 Replace golang.org/x/crypto/ssh/terminal with golang.org/x/term (#2395)
See https://github.com/golang/go/issues/31044
2021-03-20 14:38:34 +09:00
Mitsuo Heijo
8edfd14a37 Test against Golang 1.14 and 1.16 (#2396)
1.14 for 32-bit binaries
2021-03-20 12:32:44 +09:00
Junegunn Choi
1a191ec6f7 Update FUNDING.yml 2021-03-14 12:03:11 +09:00
Junegunn Choi
e7171e94b4 Update FUNDING.yml 2021-03-14 12:01:57 +09:00
Junegunn Choi
398d937419 Create FUNDING.yml 2021-03-14 11:59:56 +09:00
Junegunn Choi
34fe5ab143 0.26.0 2021-03-13 15:13:31 +09:00
Junegunn Choi
1b08f43f82 Advanced preview scroll offset expression to better support fixed header 2021-03-13 02:26:41 +09:00
Junegunn Choi
b24a2e2fdc Fix regression in preview window rendering 2021-03-12 21:23:16 +09:00
Junegunn Choi
4c4c6e626e Add support for preview window header
Fix #2373

  # Display top 3 lines as the fixed header
  fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
2021-03-12 20:32:27 +09:00
Junegunn Choi
7310370a31 Fix truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 20:31:27 +09:00
Junegunn Choi
8ae94f0059 Fix premature truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 11:05:51 +09:00
Junegunn Choi
8fccf20892 Fix incorrect tab character handling
Fix #2372
2021-03-12 10:08:18 +09:00
Charlie Vieth
5a874ae241 Speed up ANSI code processing (#2368)
This commit speeds up the parsing/processing of ANSI escape codes by
roughly 7.5x. The speedup is mostly accomplished by replacing the regex
with dedicated parsing logic (nextAnsiEscapeSequence()) and reducing the
number of allocations in extractColor().

#### Benchmarks
```
name             old time/op    new time/op     delta
ExtractColor-16    4.89µs ± 5%     0.64µs ± 2%   -86.87%  (p=0.000 n=9+9)

name             old speed      new speed       delta
ExtractColor-16  25.6MB/s ± 5%  194.6MB/s ± 2%  +661.43%  (p=0.000 n=9+9)

name             old alloc/op   new alloc/op    delta
ExtractColor-16    1.37kB ± 0%     0.31kB ± 0%   -77.31%  (p=0.000 n=10+10)

name             old allocs/op  new allocs/op   delta
ExtractColor-16      48.0 ± 0%        4.0 ± 0%   -91.67%  (p=0.000 n=10+10)
```
2021-03-11 19:34:50 +09:00
Jannik Vieten
f4e1ed25f2 [fish] Make widgets work with --option= prefix (#2383)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-03-08 22:59:56 +09:00
Junegunn Choi
cbfbb49ab4 [vim] Vim 8.0 compatibility
Fix #2367
2021-03-08 12:56:06 +09:00
solarizedalias
489b16efce [fzf-tmux] Adapt to tmux latest changes (#2379) 2021-03-08 12:44:36 +09:00
Junegunn Choi
b82c1693c0 Fix deadlocks 2021-03-08 00:08:10 +09:00
Junegunn Choi
019bfc4e35 Fix yet another deadlock
EventBox.Set should not be called while holding the terminal mutex

  goroutine 1 [semacquire]:
  sync.runtime_SemacquireMutex(0xc0001923bc, 0x1000001066200, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc0001923b8)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(...)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81
  github.com/junegunn/fzf/src.(*Terminal).Input(0xc000192000, 0x0, 0x0, 0x0, 0x0)
          /fzf/src/terminal.go:581 +0x145
  github.com/junegunn/fzf/src.Run.func10(0xc00010c8a0, 0xc000092050, 0xa)
          /fzf/src/core.go:245 +0x37
  github.com/junegunn/fzf/src.Run.func11(0xc00011a4e0)
          /fzf/src/core.go:295 +0x5ce
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a4e0, 0xc000127ec8)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.Run(0xc000180000, 0x11ac014, 0x6, 0x11ac158, 0x7)
          /fzf/src/core.go:251 +0xdac
  main.main()
          /fzf/main.go:13 +0x5a

  goroutine 11 [semacquire]:
  sync.runtime_SemacquireMutex(0xc00012c31c, 0xc00010e800, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81 +0x47
  github.com/junegunn/fzf/src/util.(*EventBox).Set(0xc00011a4e0, 0x7, 0x114eb40, 0x1265460)
          /fzf/src/util/eventbox.go:40 +0x3b
  github.com/junegunn/fzf/src.(*Terminal).killPreview(0xc000192000, 0x0)
          /fzf/src/terminal.go:1831 +0xa5
  github.com/junegunn/fzf/src.(*Terminal).exit(0xc000192000, 0xc000106e58)
          /fzf/src/terminal.go:1847 +0x75
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8.1(0xc00011a540)
          /fzf/src/terminal.go:2148 +0x38f
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a540, 0xc000106f90)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8(0xc000192000, 0xc00010a2c0)
          /fzf/src/terminal.go:2077 +0xa5
  created by github.com/junegunn/fzf/src.(*Terminal).Loop
          /fzf/src/terminal.go:2072 +0x3e8
2021-03-07 23:35:19 +09:00
Junegunn Choi
dfda5c054a [actions] Install fish using apt-get
For some reason, `test_ctrl_r` and `test_ctrl_r_abort` are not passing
on GitHub Action runner with Fish 3.2.0.
2021-03-07 22:41:27 +09:00
Junegunn Choi
f657169616 Fix deadlock on exit 2021-03-07 21:44:08 +09:00
Junegunn Choi
4c06da8b70 Fix GitHub Action build
$USER is missing
2021-03-07 18:05:39 +09:00
yoshida.shinya
9fe2393a00 Add test cases for killing input command on terminate (#2381 #2382) 2021-03-07 11:36:00 +09:00
Junegunn Choi
e2e8d94b14 Kill input command on terminate
Fix #2381
Close #2382
2021-03-07 11:30:26 +09:00
bitterfox
4f9a7f8c87 Don't exit fzf by SIGINT while executing command (#2375)
Fix #2374

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-02-28 21:01:03 +09:00
Junegunn Choi
bb0502ff44 Check gofmt in make test 2021-02-28 18:28:21 +09:00
Junegunn Choi
c256442245 Fix typo 2021-02-28 18:27:21 +09:00
Jonathan Müller
1137404190 [vim] Add keepjump to switch_back() function (#2363)
Otherwise, the jump list will contain a (hidden) entry for the FZF buffer if `window: enew` is used.
2021-02-25 21:58:13 +09:00
Junegunn Choi
d57c6d0284 Update build script to build macOS arm64 binary
Close #2361
2021-02-25 21:31:15 +09:00
Junegunn Choi
76bbf57b3d Add select and deselect actions
Close #2358
2021-02-25 21:23:05 +09:00
Hiroki Konishi
806a47a7cc [vim] Remove unnecessary border management in nvim floating window (#2370) 2021-02-25 14:41:23 +09:00
Junegunn Choi
29851c18aa [vim] Force redraw by exiting and re-entering terminal mode
Workaround for Neovim v0.5.0-dev

https://github.com/junegunn/fzf/issues/2352#issuecomment-782894123
2021-02-22 21:46:28 +09:00
Junegunn Choi
dea950c2c8 [vim] Call feedkeys only when the destination buffer is a terminal
Fix #2352
Fix https://github.com/junegunn/fzf.vim/issues/1216

Close #2364
2021-02-22 00:22:12 +09:00
Junegunn Choi
a367dfb22e README.md: Better example 2021-02-17 16:44:54 +09:00
odeson24
9fe1a7b373 Remove redundant assignment (#2356)
Co-authored-by: Ryan Ou <ryanou@aetherai.com>
2021-02-17 10:28:43 +09:00
Hussein Esmail
8e2d21c548 Update README.md (#2353)
Remove Linuxbrew links since Linuxbrew has been merged into Homebrew

* https://brew.sh/2019/02/02/homebrew-2.0.0/
2021-02-17 10:24:35 +09:00
Junegunn Choi
bedf1cd357 [vim] Use tnoremap only when it's available
Fix #2357
2021-02-17 10:04:38 +09:00
Junegunn Choi
13f180a70c [vim] Stay in terminal mode if fzf#run is called from sink
Fix #2352
2021-02-15 13:58:49 +09:00
Junegunn Choi
6654239c94 0.25.1 2021-02-03 22:32:52 +09:00
Junegunn Choi
1b61e5e9e9 Clarification on FZF_DEFAULT_COMMAND 2021-02-03 19:40:05 +09:00
Marlon Richert
43b3b907f8 [zsh] Don't run precmd hooks in cd widget (#2340)
`precmd` hooks expect the Zsh Line Editor to not be active.
Running these when the ZLE is active can lead to unpredictable results.
See https://github.com/marlonrichert/zsh-autocomplete/issues/180
2021-02-03 19:26:17 +09:00
Junegunn Choi
fcd896508b [vim] fzf#run should ignore empty 'dir' argument
Fix #2343
2021-02-03 13:51:56 +09:00
Junegunn Choi
f55c990e86 Add close action
Close #2331
2021-02-02 00:11:05 +09:00
Naveen
d110372f99 Create codeql-analysis.yml (#2338) 2021-02-01 23:54:45 +09:00
Junegunn Choi
c862af09f2 Fix toggle-preview-wrap action
Fix #2336
2021-02-01 23:14:21 +09:00
Junegunn Choi
1cfeec0ca3 Fix segmentation fault on \x1b[0K
Fix #2339
2021-02-01 22:59:11 +09:00
step
a0649edc1e [man] Clarify that $SHELL is used to run commands (#2334)
SHELL is used for execute actions and the preview and default commands.
2021-02-01 20:07:42 +09:00
Kovarththanan Rajaratnam
0e0bcb3e10 Update README.md (#2337)
--phony was renamed to --disabled in d779ff7e6d
2021-02-01 20:02:20 +09:00
Nanda Lopes
686528d627 BUILD.md: Update Go version requirement (#2332)
src/options.go:463:9: undefined: strings.ReplaceAll
    note: module requires Go 1.13make: *** [Makefile:122: target/fzf-linux_amd64] Erro 2
2021-01-28 11:43:15 +09:00
jiangjianshan
3afa920151 [install.ps1] Change permission of the downloaded binary (#2308)
Fix #2256
2021-01-28 11:41:23 +09:00
Junegunn Choi
32c493e994 [Makefile] Restore 32-bit targets
Close #2328
2021-01-20 18:40:17 +09:00
Olivier Roques
1a76bdf891 [vim] Exit terminal mode before closing FZF window (#2326)
Fix https://github.com/junegunn/fzf.vim/issues/1216
2021-01-17 22:39:40 +09:00
Junegunn Choi
af48b3df29 Replace Travis CI badge 2021-01-15 10:21:36 +09:00
freddii
58ac1fb2fa Fix typos in source code (#2322) 2021-01-15 10:10:09 +09:00
Junegunn Choi
e922704f72 Migrate to GitHub Actions 2021-01-13 19:10:24 +09:00
Vlad Doster
c6115735c7 Update README.md (#2321)
- Correct spelling/grammar
2021-01-13 04:26:10 +09:00
Ruslan Sayfutdinov
9ddf5c72be [zsh] Properly reset prompt after completion (#2318) 2021-01-13 04:09:34 +09:00
Junegunn Choi
cc5640326b [man] Fix typo 2021-01-10 04:46:31 +09:00
calvin ardi
bf447d7703 Update default number version (#2307) 2021-01-08 13:35:46 +09:00
nicolasbarra
cbb008c938 Update README to upgrade using brew upgrade (#2309) 2021-01-07 16:50:08 +09:00
E.L.K
eaa0c52b45 Fix selection changed on terminal resize (#2306) 2021-01-04 04:20:31 +09:00
Elliott Sales de Andrade
82791f7efc Use more explicit int-to-string conversion.
This fixes the following errors with Go 1.15:
```
src/options.go:452:69: conversion from untyped int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)
src/options.go:463:33: conversion from untyped int to string yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)
```
2021-01-03 14:38:09 +09:00
49 changed files with 2266 additions and 581 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]

36
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning
name: CodeQL
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ['go']
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

44
.github/workflows/linux.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
---
name: Test fzf on Linux
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go: [1.14, 1.16]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0
with:
ruby-version: 3.0.0
- name: Install packages
run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose

44
.github/workflows/macos.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
---
name: Test fzf on macOS
on:
push:
branches: [ master, devel ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: macos-latest
strategy:
matrix:
go: [1.14, 1.16]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0
with:
ruby-version: 3.0.0
- name: Install packages
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
- name: Install Ruby gems
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
- name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance
- name: Unit test
run: make test
- name: Integration test
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose

View File

@@ -1,11 +0,0 @@
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Apple Development: junegunn.c@gmail.com"
}

View File

@@ -15,9 +15,55 @@ builds:
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
post: gon .gon.hcl post: |
sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
}
EOF
gon /tmp/fzf-gon-amd64.hcl
'
- goos: - id: fzf-macos-arm
binary: fzf
goos:
- darwin
goarch:
- arm64
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
post: |
sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
}
zip {
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
}
EOF
gon /tmp/fzf-gon-arm64.hcl
'
- id: fzf
goos:
- linux - linux
- windows - windows
- freebsd - freebsd
@@ -44,6 +90,8 @@ builds:
archives: archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
builds:
- fzf
format: tar.gz format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
@@ -57,6 +105,8 @@ release:
name: fzf name: fzf
prerelease: auto prerelease: auto
name_template: '{{ .Tag }}' name_template: '{{ .Tag }}'
extra_files:
- glob: ./dist/fzf-*darwin*.zip
snapshot: snapshot:
name_template: "{{ .Tag }}-devel" name_template: "{{ .Tag }}-devel"

View File

@@ -1,28 +0,0 @@
language: go
go:
- "1.14"
env: GO111MODULE=on
os:
- linux
- osx
dist: bionic
osx_image: xcode12.2
addons:
apt:
packages:
- fish
- zsh
sources:
sourceline: ppa:fish-shell/release-3
homebrew:
packages:
- fish
- tmux
install: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
script:
- make test
# LC_ALL=C to avoid escape codes in
# printf %q $'\355\205\214\354\212\244\355\212\270' on macOS. Bash on
# macOS is built without HANDLE_MULTIBYTE?
- make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
- rubocop --require rubocop-minitest --require rubocop-performance

565
ADVANCED.md Normal file
View File

@@ -0,0 +1,565 @@
Advanced fzf examples
======================
*(Last update: 2021/05/22)*
<!-- vim-markdown-toc GFM -->
* [Introduction](#introduction)
* [Screen Layout](#screen-layout)
* [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux)
* [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources)
* [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status)
* [Branches](#branches)
* [Commit hashes](#commit-hashes)
* [Color themes](#color-themes)
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
<!-- vim-markdown-toc -->
Introduction
------------
fzf is an interactive [Unix filter][filter] program that is designed to be
used with other Unix tools. It reads a list of items from the standard input,
allows you to select a subset of the items, and prints the selected ones to
the standard output. You can think of it as an interactive version of *grep*,
and it's already useful even if you don't know any of its options.
```sh
# 1. ps: Feed the list of processes to fzf
# 2. fzf: Interactively select a process using fuzzy matching algorithm
# 3. awk: Take the PID from the selected line
# 3. kill: Kill the process with the PID
ps -ef | fzf | awk '{print $2}' | xargs kill -9
```
[filter]: https://en.wikipedia.org/wiki/Filter_(software)
While the above example succinctly summarizes the fundamental concept of fzf,
you can build much more sophisticated interactive workflows using fzf once you
learn its wide variety of features.
- To see the full list of options and features, see `man fzf`
- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md)
This document will guide you through some examples that will familiarize you
with the advanced features of fzf.
Screen Layout
-------------
### `--height`
fzf by default opens in fullscreen mode, but it's not always desirable.
Oftentimes, you want to see the current context of the terminal while using
fzf. `--height` is an option for opening fzf below the cursor in
non-fullscreen mode so you can still see the previous commands and their
results above it.
```sh
fzf --height=40%
```
![image](https://user-images.githubusercontent.com/700826/113379893-c184c680-93b5-11eb-9676-c7c0a2f01748.png)
You might also want to experiment with other layout options such as
`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc.
```sh
fzf --height=40% --layout=reverse
fzf --height=40% --layout=reverse --info=inline
fzf --height=40% --layout=reverse --info=inline --border
fzf --height=40% --layout=reverse --info=inline --border --margin=1
fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
```
![image](https://user-images.githubusercontent.com/700826/113379932-dfeac200-93b5-11eb-9e28-df1a2ee71f90.png)
*(See `Layout` section of the man page to see the full list of options)*
But you definitely don't want to repeat `--height=40% --layout=reverse
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
could write a wrapper script or shell alias, but there is an easier option.
Define `$FZF_DEFAULT_OPTS` like so:
```sh
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
```
### `fzf-tmux`
Before fzf had `--height` option, we would open fzf in a tmux split pane not
to take up the whole screen. This is done using `fzf-tmux` script.
```sh
# Open fzf on a tmux split pane below the current pane.
# Takes the same set of options.
fzf-tmux --layout=reverse
```
![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png)
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
`--height` option. But the advantage of it is that it's more flexible.
(See `man fzf-tmux` for available options.)
```sh
# On the right (50%)
fzf-tmux -r
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png)
![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png)
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
```sh
# Open tmux in a tmux popup window (default size: 50% of the screen)
fzf-tmux -p
# 80% width, 60% height
fzf-tmux -p 80%,60%
```
![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png)
> 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
Dynamic reloading of the list
-----------------------------
fzf can dynamically update the candidate list using an arbitrary program with
`reload` bindings (The design document for `reload` can be found
[here][reload]).
[reload]: https://github.com/junegunn/fzf/issues/1750
### Updating the list of processes by pressing CTRL-R
This example shows how you can set up a binding for dynamically updating the
list without restarting fzf.
```sh
(date; ps -ef) |
fzf --bind='ctrl-r:reload(date; ps -ef)' \
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
--preview='echo {}' --preview-window=down,3,wrap \
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
```
![image](https://user-images.githubusercontent.com/700826/113465047-200c7c00-946c-11eb-918c-268f37a900c8.png)
- The initial command is `(date; ps -ef)`. It prints the current date and
time, and the list of the processes.
- With `--header` option, you can show any message as the fixed header.
- To disallow selecting the first two lines (`date` and `ps` header), we use
`--header-lines=2` option.
- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that
runs `date; ps -ef`, so we can update the list of the processes by pressing
CTRL-R.
- We use simple `echo {}` preview option, so we can see the entire line on the
preview window below even if it's too long
### Toggling between data sources
You're not limiited to just one reload binding. Set up multiple bindings so
you can switch between data sources.
```sh
find * | fzf --prompt 'All> ' \
--header 'CTRL-D: Directories / CTRL-F: Files' \
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
```
![image](https://user-images.githubusercontent.com/700826/113465073-4af6d000-946c-11eb-858f-2372c0955f67.png)
![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png)
Ripgrep integration
-------------------
### Using fzf as the secondary filter
* Requires [bat][bat]
* Requires [Ripgrep][rg]
[bat]: https://github.com/sharkdp/bat
[rg]: https://github.com/BurntSushi/ripgrep
fzf is pretty fast for filtering a list that you will rarely have to think
about its performance. But it is not the right tool for searching for text
inside many large files, and in that case you should definitely use something
like [Ripgrep][rg].
In the next example, Ripgrep is the primary filter that searches for the given
text in files, and fzf is used as the secondary fuzzy filter that adds
interactivity to the workflow. And we use [bat][bat] to show the matching line in
the preview window.
This is a bash script and it will not run as expected on other non-compliant
shells. To avoid the compatibility issue, let's save this snippet as a script
file called `rfv`.
```bash
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim
IFS=: read -ra selected < <(
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
```
And run it with an initial query string.
```sh
# Make the script executable
chmod +x rfv
# Run it with the initial query "algo"
./rfv algo
```
> Ripgrep will perform the initial search and list all the lines that contain
`algo`. Then we further narrow down the list on fzf.
![image](https://user-images.githubusercontent.com/700826/113683873-a42a6200-96ff-11eb-9666-26ce4091b0e4.png)
I know it's a lot to digest, let's try to break down the code.
- Ripgrep prints the matching lines in the following format
```
man/man1/fzf.1:54:.BI "--algo=" TYPE
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
```
The first token delimited by `:` is the file path, and the second token is
the line number of the matching line. They respectively correspond to `{1}`
and `{2}` in the preview command.
- `--preview 'bat --color=always {1} --highlight-line {2}'`
- As we run `rg` with `--color=always` option, we should tell fzf to parse
ANSI color codes in the input by setting `--ansi`.
- We customize how fzf colors various text elements using `--color` option.
`-1` tells fzf to keep the original color from the input. See `man fzf` for
available color options.
- The value of `--preview-window` option consists of 5 components delimited
by `,`
1. `up` — Position of the preview window
1. `60%` — Size of the preview window
1. `border-bottom` — Preview window border only on the bottom side
1. `+{2}+3/3` — Scroll offset of the preview contents
1. `~3` — Fixed header
- Let's break down the latter two. We want to display the bat output in the
preview window with a certain scroll offset so that the matching line is
positioned near the center of the preview window.
- `+{2}` — The base offset is extracted from the second token
- `+3` — We add 3 lines to the base offset to compensate for the header
part of `bat` output
- ```
───────┬──────────────────────────────────────────────────────────
│ File: CHANGELOG.md
───────┼──────────────────────────────────────────────────────────
1 │ CHANGELOG
2 │ =========
3 │
4 │ 0.26.0
5 │ ------
```
- `/3` adjusts the offset so that the matching line is shown at a third
position in the window
- `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset
- Once we selected a line, we open the file with `vim` (`vim
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
### Using fzf as interative Ripgrep launcher
We have learned that we can bind `reload` action to a key (e.g.
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
`reload` action to `change` event** so that whenever the user *changes* the
query string on fzf, `reload` action is triggered.
Here is a variation of the above `rfv` script. fzf will restart Ripgrep every
time the user updates the query string on fzf. Searching and filtering is
completely done by Ripgrep, and fzf merely provides the interactive interface.
So we lose the "fuzziness", but the performance will be better on larger
projects, and it will free up memory as you narrow down the results.
```bash
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <(
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
```
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way
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`
- `{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
a query.
### Switching to fzf-only search mode
*(Requires fzf 0.27.1 or above)*
In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
```sh
#!/usr/bin/env bash
# Two-phase filtering with Ripgrep and fzf
#
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# * Press alt-enter to switch to fzf-only filtering
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <(
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--prompt '1. ripgrep> ' \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
```
* Phase 1. Filtering with Ripgrep
![image](https://user-images.githubusercontent.com/700826/119213880-735e8a80-bafd-11eb-8493-123e4be24fbc.png)
* Phase 2. Filtering with fzf
![image](https://user-images.githubusercontent.com/700826/119213887-7e191f80-bafd-11eb-98c9-71a1af9d7aab.png)
- We added `--prompt` option to show that fzf is initially running in "Ripgrep
launcher mode".
- We added `alt-enter` binding that
1. unbinds `change` event, so Ripgrep is no longer restarted on key press
2. changes the prompt to `2. fzf>`
3. enables search functionality of fzf
4. clears the current query string that was used to start Ripgrep process
5. and unbinds `alt-enter` itself as this is a one-off event
- We reverted `--color` option for customizing how the matching chunks are
displayed in the second phase
Log tailing
-----------
fzf can run long-running preview commands and render partial results before
completion. And when you specify `follow` flag in `--preview-window` option,
fzf will "`tail -f`" the result, automatically scrolling to the bottom.
```bash
# With "follow", preview window will automatically scroll to the bottom.
# "\033[2J" is an ANSI escape sequence for clearing the screen.
# When fzf reads this code it clears the previous preview contents.
fzf --preview-window follow --preview 'for i in $(seq 100000); do
echo "$i"
sleep 0.01
(( i % 300 == 0 )) && printf "\033[2J"
done'
```
![image](https://user-images.githubusercontent.com/700826/113473303-dd669600-94a3-11eb-88a9-1f61b996bb0e.png)
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 \
--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
```
![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
Key bindings for git objects
----------------------------
I have [blogged](https://junegunn.kr/2016/07/fzf-git) about my fzf+git key
bindings a few years ago. I'm going to show them here again, because they are
seriously useful.
### Files listed in `git status`
<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
![image](https://user-images.githubusercontent.com/700826/113473779-a9d93b00-94a6-11eb-87b5-f62a8d0a0efc.png)
### Branches
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
![image](https://user-images.githubusercontent.com/700826/113473758-87dfb880-94a6-11eb-82f4-9218103f10bd.png)
### Commit hashes
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
![image](https://user-images.githubusercontent.com/700826/113473765-91692080-94a6-11eb-8d38-ed4d41f27ac1.png)
The full source code can be found [here](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236).
Color themes
------------
You can customize how fzf colors the text elements with `--color` option. Here
are a few color themes. Note that you need a terminal emulator that can
display 24-bit colors.
```sh
# junegunn/seoul256.vim (dark)
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
```
![seoul256](https://user-images.githubusercontent.com/700826/113475011-2c192d80-94ae-11eb-9d17-1e5867bae01f.png)
```sh
# junegunn/seoul256.vim (light)
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
```
![seoul256-light](https://user-images.githubusercontent.com/700826/113475022-389d8600-94ae-11eb-905f-0939dd535837.png)
```sh
# morhetz/gruvbox
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
```
![gruvbox](https://user-images.githubusercontent.com/700826/113475042-494dfc00-94ae-11eb-9322-cd03a027305a.png)
```sh
# arcticicestudio/nord-vim
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
```
![nord](https://user-images.githubusercontent.com/700826/113475063-67b3f780-94ae-11eb-9b24-5f0d22b63399.png)
```sh
# tomasr/molokai
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
```
![molokai](https://user-images.githubusercontent.com/700826/113475085-8619f300-94ae-11eb-85e4-2766fc3246bf.png)
### Generating fzf color theme from Vim color schemes
The Vim plugin of fzf can generate `--color` option from the current color
scheme according to `g:fzf_colors` variable. You can find the detailed
explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors).
Here is an example. Add this to your Vim configuration file.
```vim
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'preview-bg': ['bg', 'NormalFloat'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
```
Then you can see how the `--color` option is generated by printing the result
of `fzf#wrap()`.
```vim
:echo fzf#wrap()
```
Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of
the current file.
```vim
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))
```

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.11 or above - Go 1.13 or above
### Using Makefile ### Using Makefile

View File

@@ -1,6 +1,77 @@
CHANGELOG CHANGELOG
========= =========
0.27.1
------
- Added `unbind` action. In the following Ripgrep launcher example, you can
use `unbind(reload)` to switch to fzf-only filtering mode.
- See https://github.com/junegunn/fzf/blob/master/ADVANCED.md#switching-to-fzf-only-search-mode
- Vim plugin
- Vim plugin will stop immediately even when the source command hasn't finished
```vim
" 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
```vim
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
```
0.27.0
------
- More border options for `--preview-window`
```sh
fzf --preview 'cat {}' --preview-window border-left
fzf --preview 'cat {}' --preview-window border-left --border horizontal
fzf --preview 'cat {}' --preview-window top:border-bottom
fzf --preview 'cat {}' --preview-window top:border-horizontal
```
- Automatically set `/dev/tty` as STDIN on execute action
```sh
# Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal"
# ls | fzf --bind "enter:execute(vim {} < /dev/tty)"
# "< /dev/tty" part is no longer needed
ls | fzf --bind "enter:execute(vim {})"
```
- Bug fixes and improvements
- Signed and notarized macOS binaries
(Huge thanks to [BACKERS.md](https://github.com/junegunn/junegunn/blob/main/BACKERS.md)!)
0.26.0
------
- Added support for fixed header in preview window
```sh
# Display top 3 lines as the fixed header
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
```
- More advanced preview offset expression to better support the fixed header
```sh
# Preview with bat, matching line in the middle of the window below
# the fixed header of the top 3 lines
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
git grep --line-number '' |
fzf --delimiter : \
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
--preview-window '~3:+{2}+3/2'
```
- 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
- Bug fixes and improvements
- Built with Go 1.16
0.25.1
------
- Added `close` action
- Close preview window if open, abort fzf otherwise
- Bug fixes and improvements
0.25.0 0.25.0
------ ------
- Text attributes set in `--color` are not reset when fzf sees another - Text attributes set in `--color` are not reset when fzf sees another

View File

@@ -27,6 +27,7 @@ $(error Not on git repository; cannot determine $$FZF_REVISION)
endif endif
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
@@ -40,6 +41,10 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),armv5l) else ifeq ($(UNAME_M),armv5l)
BINARY := $(BINARYARM5) BINARY := $(BINARYARM5)
else ifeq ($(UNAME_M),armv6l) else ifeq ($(UNAME_M),armv6l)
@@ -61,12 +66,16 @@ endif
all: target/$(BINARY) all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util github.com/junegunn/fzf/src/util
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
install: bin/fzf install: bin/fzf
build: build:
@@ -110,6 +119,9 @@ endif
clean: clean:
$(RM) -r dist target $(RM) -r dist target
target/$(BINARY32): $(SOURCES)
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES) target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
@@ -144,4 +156,4 @@ update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all build release test install clean docker docker-test update .PHONY: all build release test bench install clean docker docker-test update

View File

@@ -127,9 +127,15 @@ let g:fzf_action = {
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
" - Popup window " - Popup window (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" - Popup window (center of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
@@ -302,6 +308,7 @@ following options are allowed:
- Optional: - Optional:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false]
- `border` [string default `rounded`]: Border style - `border` [string default `rounded`]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
@@ -362,7 +369,7 @@ Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS /tmp` is possible. to it, so that something like `:LS /tmp` is possible.
```vim ```vim
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
``` ```
@@ -372,7 +379,7 @@ a unique name to our command and pass it as the first argument to `fzf#wrap`.
```vim ```vim
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined. " The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
``` ```
@@ -392,15 +399,41 @@ Tips
### fzf inside terminal buffer ### fzf inside terminal buffer
The latest versions of Vim and Neovim include builtin terminal emulator On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
(`:terminal`) and fzf will start in a terminal buffer in the following cases: If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
in Neovim.
- On Neovim ```vim
- On GVim " Terminal colors for seoul256 color scheme
- On Terminal Vim with a non-default layout if has('nvim')
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` let g:terminal_color_0 = '#4e4e4e'
let g:terminal_color_1 = '#d68787'
let g:terminal_color_2 = '#5f865f'
let g:terminal_color_3 = '#d8af5f'
let g:terminal_color_4 = '#85add4'
let g:terminal_color_5 = '#d7afaf'
let g:terminal_color_6 = '#87afaf'
let g:terminal_color_7 = '#d0d0d0'
let g:terminal_color_8 = '#626262'
let g:terminal_color_9 = '#d75f87'
let g:terminal_color_10 = '#87af87'
let g:terminal_color_11 = '#ffd787'
let g:terminal_color_12 = '#add4fb'
let g:terminal_color_13 = '#ffafaf'
let g:terminal_color_14 = '#87d7d7'
let g:terminal_color_15 = '#e4e4e4'
else
let g:terminal_ansi_colors = [
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
\ '#626262', '#d75f87', '#87af87', '#ffd787',
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
\ ]
endif
```
#### Starting fzf in a popup window ### Starting fzf in a popup window
```vim ```vim
" Required: " Required:
@@ -410,6 +443,7 @@ The latest versions of Vim and Neovim include builtin terminal emulator
" Optional: " Optional:
" - xoffset [float default 0.5 range [0 ~ 1]] " - xoffset [float default 0.5 range [0 ~ 1]]
" - yoffset [float default 0.5 range [0 ~ 1]] " - yoffset [float default 0.5 range [0 ~ 1]]
" - relative [boolean default v:false]
" - border [string default 'rounded']: Border style " - border [string default 'rounded']: Border style
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
@@ -427,21 +461,21 @@ else
endif endif
``` ```
#### Hide statusline ### Hide statusline
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of `fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
the window. the window.
For example, if you use a non-popup layout (e.g. `{'down': '40%'}`) on Neovim, For example, if you open fzf on the bottom on the screen (e.g. `{'down':
you might want to temporarily disable the statusline for a cleaner look. '40%'}`), you might want to temporarily disable the statusline for a cleaner
look.
```vim ```vim
if has('nvim') && !exists('g:fzf_layout') let g:fzf_layout = { 'down': '30%' }
autocmd! FileType fzf autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler \| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
``` ```
[License](LICENSE) [License](LICENSE)

View File

@@ -1,4 +1,4 @@
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![github-actions](https://github.com/junegunn/fzf/workflows/Test%20fzf%20on%20Linux/badge.svg)](https://github.com/junegunn/fzf/actions)
=== ===
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
@@ -17,7 +17,7 @@ Pros
- The most comprehensive feature set - The most comprehensive feature set
- Flexible layout - Flexible layout
- Batteries included - Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion - Vim/Neovim plugin, key bindings, and fuzzy auto-completion
Table of Contents Table of Contents
----------------- -----------------
@@ -25,7 +25,7 @@ Table of Contents
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Installation](#installation) * [Installation](#installation)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) * [Using Homebrew](#using-homebrew)
* [Using git](#using-git) * [Using git](#using-git)
* [Using Linux package managers](#using-linux-package-managers) * [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows) * [Windows](#windows)
@@ -84,9 +84,9 @@ stuff.
[bin]: https://github.com/junegunn/fzf/releases [bin]: https://github.com/junegunn/fzf/releases
### Using Homebrew or Linuxbrew ### Using Homebrew
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/) You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
to install fzf. to install fzf.
```sh ```sh
@@ -166,12 +166,12 @@ For more installation options, see [README-VIM.md](README-VIM.md).
Upgrading fzf Upgrading fzf
------------- -------------
fzf is being actively developed and you might want to upgrade it once in a fzf is being actively developed, and you might want to upgrade it once in a
while. Please follow the instruction below depending on the installation while. Please follow the instruction below depending on the installation
method used. method used.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew update; brew reinstall fzf` - brew: `brew update; brew upgrade fzf`
- macports: `sudo port upgrade fzf` - macports: `sudo port upgrade fzf`
- chocolatey: `choco upgrade fzf` - chocolatey: `choco upgrade fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
@@ -217,7 +217,7 @@ cursor with `--height` option.
vim $(fzf --height 40%) vim $(fzf --height 40%)
``` ```
Also check out `--reverse` and `--layout` options if you prefer Also, check out `--reverse` and `--layout` options if you prefer
"top-down" layout instead of the default "bottom-up" layout. "top-down" layout instead of the default "bottom-up" layout.
```sh ```sh
@@ -264,6 +264,13 @@ or `py`.
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- e.g. `export FZF_DEFAULT_COMMAND='fd --type f'` - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
- > :warning: This variable is not used by shell extensions due to the
> slight difference in requirements.
>
> (e.g. `CTRL-T` runs `$FZF_CTRL_T_COMMAND` instead, `vim **<tab>` runs
> `_fzf_compgen_path()`, and `cd **<tab>` runs `_fzf_compgen_dir()`)
>
> The available options are described later in this document.
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
@@ -282,9 +289,10 @@ If you learn by watching videos, check out this screencast by [@samoshkin](https
Examples Examples
-------- --------
Many useful examples can be found on [the wiki * [Wiki page of examples](https://github.com/junegunn/fzf/wiki/examples)
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your * *Disclaimer: The examples on this page are maintained by the community
own as well. and are not thoroughly tested*
* [Advanced fzf examples](https://github.com/junegunn/fzf/blob/master/ADVANCED.md)
`fzf-tmux` script `fzf-tmux` script
----------------- -----------------
@@ -331,7 +339,7 @@ fish.
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options - Set `FZF_ALT_C_OPTS` to pass additional options
If you're on a tmux session, you can start fzf in a tmux split pane or in If you're on a tmux session, you can start fzf in a tmux split-pane or in
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`). a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
See `fzf-tmux --help` for available options. See `fzf-tmux --help` for available options.
@@ -343,12 +351,12 @@ Fuzzy completion for bash and zsh
#### Files and directories #### Files and directories
Fuzzy completion for files and directories can be triggered if the word before Fuzzy completion for files and directories can be triggered if the word before
the cursor ends with the trigger sequence which is by default `**`. the cursor ends with the trigger sequence, which is by default `**`.
- `COMMAND [DIRECTORY/][FUZZY_PATTERN]**<TAB>` - `COMMAND [DIRECTORY/][FUZZY_PATTERN]**<TAB>`
```sh ```sh
# Files under current directory # Files under the current directory
# - You can select multiple items with TAB key # - You can select multiple items with TAB key
vim **<TAB> vim **<TAB>
@@ -372,7 +380,7 @@ cd ~/github/fzf**<TAB>
#### Process IDs #### Process IDs
Fuzzy completion for PIDs is provided for kill command. In this case, Fuzzy completion for PIDs is provided for kill command. In this case,
there is no trigger sequence, just press tab key after kill command. there is no trigger sequence; just press the tab key after the kill command.
```sh ```sh
# Can select multiple processes with <TAB> or <Shift-TAB> keys # Can select multiple processes with <TAB> or <Shift-TAB> keys
@@ -381,7 +389,7 @@ kill -9 <TAB>
#### Host names #### Host names
For ssh and telnet commands, fuzzy completion for host names is provided. The For ssh and telnet commands, fuzzy completion for hostnames is provided. The
names are extracted from /etc/hosts and ~/.ssh/config. names are extracted from /etc/hosts and ~/.ssh/config.
```sh ```sh
@@ -404,7 +412,7 @@ unalias **<TAB>
export FZF_COMPLETION_TRIGGER='~~' export FZF_COMPLETION_TRIGGER='~~'
# Options to fzf command # Options to fzf command
export FZF_COMPLETION_OPTS='+c -x' export FZF_COMPLETION_OPTS='--border --info=inline'
# Use fd (https://github.com/sharkdp/fd) instead of the default find # Use fd (https://github.com/sharkdp/fd) instead of the default find
# command for listing path candidates. # command for listing path candidates.
@@ -469,11 +477,11 @@ _fzf_complete_doge() {
- The arguments before `--` are the options to fzf. - The arguments before `--` are the options to fzf.
- After `--`, simply pass the original completion arguments unchanged (`"$@"`). - After `--`, simply pass the original completion arguments unchanged (`"$@"`).
- Then write a set of commands that generates the completion candidates and - Then, write a set of commands that generates the completion candidates and
feed its output to the function using process substitution (`< <(...)`). feed its output to the function using process substitution (`< <(...)`).
zsh will automatically pick up the function using the naming convention but in zsh will automatically pick up the function using the naming convention but in
bash you have to manually associate the function with the command using bash you have to manually associate the function with the command using the
`complete` command. `complete` command.
```sh ```sh
@@ -509,12 +517,12 @@ Advanced topics
fzf is fast and is [getting even faster][perf]. Performance should not be fzf is fast and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the a problem in most use cases. However, you might want to be aware of the
options that affect the performance. options that affect performance.
- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it - `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it
makes the initial scanning slower. So it's not recommended that you add it makes the initial scanning slower. So it's not recommended that you add it
to your `$FZF_DEFAULT_OPTS`. to your `$FZF_DEFAULT_OPTS`.
- `--nth` makes fzf slower as fzf has to tokenize each line. - `--nth` makes fzf slower because it has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each - `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line. line.
- If you absolutely need better performance, you can consider using - If you absolutely need better performance, you can consider using
@@ -563,17 +571,17 @@ FZF_DEFAULT_COMMAND='find . -type f' \
#### 3. Interactive ripgrep integration #### 3. Interactive ripgrep integration
The following example uses fzf as the selector interface for ripgrep. We bound 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, ripgrep `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 process will restart with the updated query string denoted by the placeholder
expression `{q}`. Also, note that we used `--phony` option so that fzf doesn't expression `{q}`. Also, note that we used `--disabled` option so that fzf
perform any secondary filtering. doesn't perform any secondary filtering.
```sh ```sh
INITIAL_QUERY="" INITIAL_QUERY=""
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \ FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
fzf --bind "change:reload:$RG_PREFIX {q} || true" \ fzf --bind "change:reload:$RG_PREFIX {q} || true" \
--ansi --phony --query "$INITIAL_QUERY" \ --ansi --disabled --query "$INITIAL_QUERY" \
--height=50% --layout=reverse --height=50% --layout=reverse
``` ```
@@ -589,7 +597,7 @@ Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`.
The window can be scrolled using the mouse or custom key bindings. The window can be scrolled using the mouse or custom key bindings.
```bash ```bash
# {} is replaced to the single-quoted string of the focused line # {} is replaced with the single-quoted string of the focused line
fzf --preview 'cat {}' fzf --preview 'cat {}'
``` ```
@@ -608,7 +616,7 @@ You can customize the size, position, and border of the preview window using
```bash ```bash
fzf --height 40% --layout reverse --info inline --border \ fzf --height 40% --layout reverse --info inline --border \
--preview 'file {}' --preview-window down:1:noborder \ --preview 'file {}' --preview-window up,1,border-horizontal \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
``` ```
@@ -661,7 +669,7 @@ fzf
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
If you want the command to follow symbolic links, and don't want it to exclude If you want the command to follow symbolic links and don't want it to exclude
hidden files, use the following command: hidden files, use the following command:
```sh ```sh

View File

@@ -204,7 +204,16 @@ if [[ "$opt" =~ "-K -E" ]]; then
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
# tmux dropped the support for `-K`, `-R` to popup command
# TODO: We can remove this once tmux 3.2 is released
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
opt="${opt/-K/}"
else
opt="${opt} -R"
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
exit $? exit $?
fi fi

View File

@@ -1,4 +1,4 @@
fzf.txt fzf Last change: January 3 2021 fzf.txt fzf Last change: May 19 2021
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -14,8 +14,8 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to
Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap| Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap|
Tips |fzf-tips| Tips |fzf-tips|
fzf inside terminal buffer |fzf-inside-terminal-buffer| fzf inside terminal buffer |fzf-inside-terminal-buffer|
Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window| Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window|
Hide statusline |fzf-hide-statusline| Hide statusline |fzf-hide-statusline|
License |fzf-license| License |fzf-license|
FZF VIM INTEGRATION *fzf-vim-integration* FZF VIM INTEGRATION *fzf-vim-integration*
@@ -155,9 +155,15 @@ Examples~
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
" Default fzf layout " Default fzf layout
" - Popup window " - Popup window (center of the screen)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" - Popup window (center of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
" - Popup window (anchored to the bottom of the current window)
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '40%' } let g:fzf_layout = { 'down': '40%' }
@@ -318,6 +324,7 @@ following options are allowed:
- Optional: - Optional:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false]
- `border` [string default `rounded`]: Border style - `border` [string default `rounded`]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
@@ -372,7 +379,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
Our `:LS` command will be much more useful if we can pass a directory argument Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS/tmp` is possible. to it, so that something like `:LS/tmp` is possible.
> >
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
< <
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
@@ -380,7 +387,7 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
> >
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir. " The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
" The name is ignored if g:fzf_history_dir is not defined. " The name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
< <
@@ -412,8 +419,46 @@ The latest versions of Vim and Neovim include builtin terminal emulator
- On Terminal Vim with a non-default layout - On Terminal Vim with a non-default layout
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}` - `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}`
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
If you find the default ANSI colors to be different, consider configuring the
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
in Neovim.
Starting fzf in a popup window~ *g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13*
*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9*
*g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5*
*g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1*
*g:terminal_color_0*
>
" Terminal colors for seoul256 color scheme
if has('nvim')
let g:terminal_color_0 = '#4e4e4e'
let g:terminal_color_1 = '#d68787'
let g:terminal_color_2 = '#5f865f'
let g:terminal_color_3 = '#d8af5f'
let g:terminal_color_4 = '#85add4'
let g:terminal_color_5 = '#d7afaf'
let g:terminal_color_6 = '#87afaf'
let g:terminal_color_7 = '#d0d0d0'
let g:terminal_color_8 = '#626262'
let g:terminal_color_9 = '#d75f87'
let g:terminal_color_10 = '#87af87'
let g:terminal_color_11 = '#ffd787'
let g:terminal_color_12 = '#add4fb'
let g:terminal_color_13 = '#ffafaf'
let g:terminal_color_14 = '#87d7d7'
let g:terminal_color_15 = '#e4e4e4'
else
let g:terminal_ansi_colors = [
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
\ '#626262', '#d75f87', '#87af87', '#ffd787',
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
\ ]
endif
<
< Starting fzf in a popup window >____________________________________________~
*fzf-starting-fzf-in-a-popup-window* *fzf-starting-fzf-in-a-popup-window*
> >
" Required: " Required:
@@ -423,6 +468,7 @@ Starting fzf in a popup window~
" Optional: " Optional:
" - xoffset [float default 0.5 range [0 ~ 1]] " - xoffset [float default 0.5 range [0 ~ 1]]
" - yoffset [float default 0.5 range [0 ~ 1]] " - yoffset [float default 0.5 range [0 ~ 1]]
" - relative [boolean default v:false]
" - border [string default 'rounded']: Border style " - border [string default 'rounded']: Border style
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
@@ -438,21 +484,21 @@ or above) by putting fzf-tmux options in `tmux` key.
endif endif
< <
Hide statusline~ < Hide statusline >___________________________________________________________~
*fzf-hide-statusline* *fzf-hide-statusline*
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of `fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of
the window. the window.
For example, if you use a non-popup layout (e.g. `{'down':'40%'}`) on Neovim, For example, if you open fzf on the bottom on the screen (e.g. `{'down':
you might want to temporarily disable the statusline for a cleaner look. '40%'}`), you might want to temporarily disable the statusline for a cleaner
look.
> >
if has('nvim') && !exists('g:fzf_layout') let g:fzf_layout = { 'down': '30%' }
autocmd! FileType fzf autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler \| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
< <
LICENSE *fzf-license* LICENSE *fzf-license*

16
go.mod
View File

@@ -2,14 +2,16 @@ module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell v1.4.0 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-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-runewidth v0.0.12
github.com/mattn/go-shellwords v1.0.10 github.com/mattn/go-shellwords v1.0.11
github.com/saracen/walker v0.1.1 github.com/rivo/uniseg v0.2.0
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 github.com/saracen/walker v0.1.2
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
golang.org/x/text v0.3.3 // indirect golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
golang.org/x/text v0.3.6 // indirect
) )
go 1.13 go 1.13

42
go.sum
View File

@@ -2,34 +2,32 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/saracen/walker v0.1.1 h1:Ou2QIKTWqo0QxhtuHVmtObbmhjMCEUyJ82xp0uV+MGI= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/saracen/walker v0.1.1/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 h1:/DtoiOYKoQCcIFXQjz07RnWNPRCbqmSXSpgEzhC9ZHM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.25.0 version=0.27.1
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -168,7 +168,8 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;; Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;; Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;; Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;; Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;

View File

@@ -1,4 +1,4 @@
$version="0.25.0" $version="0.27.1"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
@@ -43,7 +43,11 @@ function download {
$url="https://github.com/junegunn/fzf/releases/download/$version/$file" $url="https://github.com/junegunn/fzf/releases/download/$version/$file"
$temp=$env:TMP + "\fzf.zip" $temp=$env:TMP + "\fzf.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp")) if ($PSVersionTable.PSVersion.Major -ge 3) {
Invoke-WebRequest -Uri $url -OutFile $temp
} else {
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
}
if ($?) { if ($?) {
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp) (Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
} else { } else {
@@ -53,7 +57,7 @@ function download {
$binary_error="Failed to download $file" $binary_error="Failed to download $file"
return return
} }
check_binary >$null echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
} }
download "fzf-$version-windows_amd64.zip" download "fzf-$version-windows_amd64.zip"

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.24" var version string = "0.27"
var revision string = "devel" var revision string = "devel"
func main() { 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jan 2021" "fzf 0.25.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "May 2021" "fzf 0.27.1" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jan 2021" "fzf 0.25.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "May 2021" "fzf 0.27.1" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -74,7 +74,7 @@ Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.BI "--disabled" .BI "--disabled"
Do not perform search. With this option, fzf becomes a simple selector Do not perform search. With this option, fzf becomes a simple selector
interface rather than a "fuzzy finder". You can later enable the search using interface rather than a "fuzzy finder". You can later enable the search using
\fBenable-search\fR or `\fBtoggle-search\R action. \fBenable-search\fR or \fBtoggle-search\fR action.
.SS Search result .SS Search result
.TP .TP
.B "+s, --no-sort" .B "+s, --no-sort"
@@ -203,6 +203,8 @@ Draw border around the finder
.br .br
.BR right .BR right
.br .br
.BR none
.br
.TP .TP
.B "--no-unicode" .B "--no-unicode"
@@ -442,7 +444,7 @@ e.g.
done'\fR done'\fR
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]" .BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@@ -477,35 +479,51 @@ e.g.
* Cyclic scrolling is enabled with \fB:cycle\fR flag. * Cyclic scrolling is enabled with \fB:cycle\fR flag.
* To change the style of the border of the preview window, specify one of * To change the style of the border of the preview window, specify one of
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with the options for \fB--border\fR with \fBborder-\fR prefix.
sharp edges), or \fBnoborder\fR (no border). e.g. \fBborder-rounded\fR (border with rounded edges, default),
\fBborder-sharp\fR (border with sharp edges), \fBborder-left\fR,
\fBborder-none\fR, etc.
* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview * \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
window. \fBSCROLL\fR can be either a numeric integer or a single-field index preview window.
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
for adjusting the base offset so that you can see the text above it. It should - \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height. - The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR).
- The final \fB/DENOM\fR part is for specifying a fraction of the preview window height.
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
are always visible.
* \fBdefault\fR resets all options previously set to the default. * \fBdefault\fR resets all options previously set to the default.
.RS .RS
e.g. e.g.
\fB# Non-default scroll window positions and sizes \fB# Non-default scroll window positions and sizes
fzf --preview="head {}" --preview-window=up:30% fzf --preview="head {}" --preview-window=up,30%
fzf --preview="file {}" --preview-window=down:1 fzf --preview="file {}" --preview-window=down,1
# Initial scroll offset is set to the line number of each line of # Initial scroll offset is set to the line number of each line of
# git grep output *minus* 5 lines (-5) # git grep output *minus* 5 lines (-5)
git grep --line-number '' | git grep --line-number '' |
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5 fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5'
# Preview with bat, matching line in the middle of the window (-/2) # Preview with bat, matching line in the middle of the window below
# the fixed header of the top 3 lines
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
git grep --line-number '' | git grep --line-number '' |
fzf --delimiter : \\ fzf --delimiter : \\
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\ --preview 'bat --style=full --color=always --highlight-line {2} {1}' \\
--preview-window +{2}-/2\fR --preview-window '~3,+{2}+3/2'
# Display top 3 lines as the fixed header
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
.RE .RE
.SS Scripting .SS Scripting
@@ -514,10 +532,12 @@ e.g.
Start the finder with the given query Start the finder with the given query
.TP .TP
.B "-1, --select-1" .B "-1, --select-1"
Automatically select the only match If there is only one match for the initial query (\fB--query\fR), do not start
interactive finder and automatically select the only match
.TP .TP
.B "-0, --exit-0" .B "-0, --exit-0"
Exit immediately when there's no match If there is no match for the initial query (\fB--query\fR), do not start
interactive finder and exit immediately
.TP .TP
.BI "-f, --filter=" "STR" .BI "-f, --filter=" "STR"
Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
@@ -570,7 +590,8 @@ Note that most options have the opposite versions with \fB--no-\fR prefix.
.TP .TP
.B FZF_DEFAULT_COMMAND .B FZF_DEFAULT_COMMAND
Default command to use when input is tty. On *nix systems, fzf runs the command Default command to use when input is tty. On *nix systems, fzf runs the command
with \fBsh -c\fR, so make sure that it's POSIX-compliant. with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in
this case make sure that the command is POSIX-compliant.
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
@@ -779,9 +800,11 @@ A key or an event can be bound to one or more of the following actions.
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection) \fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string) \fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR \fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) \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) \fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality) \fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
@@ -817,6 +840,7 @@ A key or an event can be bound to one or more of the following actions.
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
@@ -828,6 +852,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
@@ -890,6 +915,10 @@ executes the command without the switching. Note that fzf will not be
responsive until the command is complete. For asynchronous execution, start responsive until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR). your command as a background process (i.e. appending \fB&\fR).
On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list

View File

@@ -154,46 +154,79 @@ function! fzf#install()
endif endif
endfunction endfunction
function! s:version_requirement(val, min) let s:versions = {}
let val = split(a:val, '\.') function s:get_version(bin)
let min = split(a:min, '\.') if has_key(s:versions, a:bin)
for idx in range(0, len(min) - 1) return s:versions[a:bin]
let v = get(val, idx, 0) end
if v < min[idx] | return 0 let command = a:bin . ' --version'
elseif v > min[idx] | return 1 let output = systemlist(command)
if v:shell_error || empty(output)
return ''
endif
let ver = matchstr(output[-1], '[0-9.]\+')
let s:versions[a:bin] = ver
return ver
endfunction
function! s:compare_versions(a, b)
let a = split(a:a, '\.')
let b = split(a:b, '\.')
for idx in range(0, max([len(a), len(b)]) - 1)
let v1 = str2nr(get(a, idx, 0))
let v2 = str2nr(get(b, idx, 0))
if v1 < v2 | return -1
elseif v1 > v2 | return 1
endif endif
endfor endfor
return 1 return 0
endfunction
function! s:compare_binary_versions(a, b)
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
endfunction endfunction
let s:checked = {} let s:checked = {}
function! fzf#exec(...) function! fzf#exec(...)
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) let binaries = []
let s:exec = s:fzf_go if executable('fzf')
elseif executable('fzf') call add(binaries, 'fzf')
let s:exec = 'fzf'
elseif input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
call fzf#install()
return fzf#exec()
else
redraw
throw 'fzf executable not found'
endif endif
if executable(s:fzf_go)
call add(binaries, s:fzf_go)
endif
if empty(binaries)
if input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
call fzf#install()
return fzf#exec()
else
redraw
throw 'fzf executable not found'
endif
elseif len(binaries) > 1
call sort(binaries, 's:compare_binary_versions')
endif
let s:exec = binaries[-1]
endif endif
if a:0 && !has_key(s:checked, a:1) if a:0 && !has_key(s:checked, a:1)
let command = s:exec . ' --version' let fzf_version = s:get_version(s:exec)
let output = systemlist(command) if empty(fzf_version)
if v:shell_error || empty(output) let message = printf('Failed to run "%s --version"', s:exec)
throw printf('Failed to run "%s": %s', command, output) unlet s:exec
endif throw message
let fzf_version = matchstr(output[-1], '[0-9.]\+') end
if s:version_requirement(fzf_version, a:1)
if s:compare_versions(fzf_version, a:1) >= 0
let s:checked[a:1] = 1 let s:checked[a:1] = 1
return s:exec return s:exec
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y' elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
let s:versions = {}
unlet s:exec
redraw redraw
call fzf#install() call fzf#install()
return fzf#exec(a:1, 1) return fzf#exec(a:1, 1)
@@ -424,27 +457,27 @@ try
throw v:exception throw v:exception
endtry endtry
if !has_key(dict, 'dir') if !s:present(dict, 'dir')
let dict.dir = s:fzf_getcwd() let dict.dir = s:fzf_getcwd()
endif endif
if has('win32unix') && has_key(dict, 'dir') if has('win32unix') && s:present(dict, 'dir')
let dict.dir = fnamemodify(dict.dir, ':p') let dict.dir = fnamemodify(dict.dir, ':p')
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
let source = dict.source let source = remove(dict, 'source')
let type = type(source) let type = type(source)
if type == 1 if type == 1
let prefix = '( '.source.' )|' let source_command = source
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call writefile(source, temps.input)
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|' let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
else else
throw 'Invalid source type' throw 'Invalid source type'
endif endif
else else
let prefix = '' let source_command = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux') let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@@ -454,20 +487,24 @@ try
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_height if use_term
let optstr .= ' --no-height'
elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --height='.height
elseif use_term
let optstr .= ' --no-height'
endif endif
let optstr .= s:border_opt(get(dict, 'window', 0)) let optstr .= s:border_opt(get(dict, 'window', 0))
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
@@ -478,6 +515,13 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
if len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote] let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry endtry
endfunction endfunction
@@ -507,8 +551,8 @@ function! s:fzf_tmux(dict)
endif endif
endfor endfor
endif endif
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s - --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -638,8 +682,7 @@ function! s:execute(dict, command, use_height, temps) abort
let a:temps.shellscript = shellscript let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else else
execute 'silent !'.command execute 'silent !'.command
endif endif
@@ -764,6 +807,13 @@ function! s:split(dict)
endtry endtry
endfunction endfunction
nnoremap <silent> <Plug>(fzf-insert) i
nnoremap <silent> <Plug>(fzf-normal) <Nop>
if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
endif
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
@@ -776,7 +826,7 @@ function! s:execute_term(dict, command, temps) abort
function! fzf.switch_back(inplace) function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf if a:inplace && bufnr('') == self.buf
if bufexists(self.pbuf) if bufexists(self.pbuf)
execute 'keepalt b' self.pbuf execute 'keepalt keepjumps b' self.pbuf
endif endif
" No other listed buffer " No other listed buffer
if bufnr('') == self.buf if bufnr('') == self.buf
@@ -816,6 +866,10 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(self.dict) call s:pushd(self.dict)
call s:callback(self.dict, lines) call s:callback(self.dict, lines)
call self.switch_back(s:getpos() == self.ppos) call self.switch_back(s:getpos() == self.ppos)
if &buftype == 'terminal'
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
endif
endfunction endfunction
try try
@@ -831,16 +885,16 @@ function! s:execute_term(dict, command, temps) abort
if has('nvim') if has('nvim')
call termopen(command, fzf) call termopen(command, fzf)
else else
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'} let term_opts = {'exit_cb': function(fzf.on_exit)}
if v:version >= 802
let term_opts.term_kill = 'term'
endif
if is_popup if is_popup
let term_opts.hidden = 1 let term_opts.hidden = 1
else else
let term_opts.curwin = 1 let term_opts.curwin = 1
endif endif
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts) let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
if exists('&termwinkey')
call setbufvar(fzf.buf, '&termwinkey', '<c-z>')
endif
if is_popup && exists('#TerminalWinOpen') if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen doautocmd <nomodeline> TerminalWinOpen
endif endif
@@ -849,6 +903,9 @@ function! s:execute_term(dict, command, temps) abort
endif endif
endif endif
tnoremap <buffer> <c-z> <nop> tnoremap <buffer> <c-z> <nop>
if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '<c-w>')
tnoremap <buffer> <c-w> <c-w>.
endif
finally finally
call s:dopopd() call s:dopopd()
endtry endtry
@@ -904,13 +961,9 @@ if has('nvim')
function s:create_popup(hl, opts) abort function s:create_popup(hl, opts) abort
let buf = nvim_create_buf(v:false, v:true) let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
let win = nvim_open_win(buf, v:true, opts) let win = nvim_open_win(buf, v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl) call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
call setwinvar(win, '&colorcolumn', '') call setwinvar(win, '&colorcolumn', '')
if !empty(border)
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
endif
return buf return buf
endfunction endfunction
else else
@@ -929,11 +982,19 @@ else
endif endif
function! s:popup(opts) abort function! s:popup(opts) abort
let xoffset = get(a:opts, 'xoffset', 0.5)
let yoffset = get(a:opts, 'yoffset', 0.5)
let relative = get(a:opts, 'relative', 0)
" Use current window size for positioning relatively positioned popups
let columns = relative ? winwidth(0) : &columns
let lines = relative ? winheight(0) : (&lines - has('nvim'))
" Size and position " Size and position
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(&columns * a:opts.width)]), &columns]) let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns])
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(&lines * a:opts.height)]), &lines - has('nvim')]) let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines])
let row = float2nr(get(a:opts, 'yoffset', 0.5) * (&lines - height)) let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
let col = float2nr(get(a:opts, 'xoffset', 0.5) * (&columns - width)) let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
" Managing the differences " Managing the differences
let row = min([max([0, row]), &lines - has('nvim') - height]) let row = min([max([0, row]), &lines - has('nvim') - height])

View File

@@ -211,7 +211,6 @@ _fzf_complete() {
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle reset-prompt
command rm -f "$fifo" command rm -f "$fifo"
} }
@@ -302,6 +301,7 @@ fzf-completion() {
if eval "type _fzf_complete_${cmd} > /dev/null"; then if eval "type _fzf_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf} prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
zle reset-prompt
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" "$lbuf" _fzf_dir_completion "$prefix" "$lbuf"
else else

View File

@@ -20,6 +20,7 @@ function fzf_key_bindings
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -l dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden. # $dir itself, even if hidden.
@@ -42,6 +43,7 @@ function fzf_key_bindings
commandline -t "" commandline -t ""
end end
for i in $result for i in $result
commandline -it -- $prefix
commandline -it -- (string escape $i) commandline -it -- (string escape $i)
commandline -it -- ' ' commandline -it -- ' '
end end
@@ -74,6 +76,7 @@ function fzf_key_bindings
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1] set -l dir $commandline[1]
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND " test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
@@ -88,6 +91,7 @@ function fzf_key_bindings
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""
commandline -it -- $prefix
end end
end end
@@ -116,9 +120,15 @@ function fzf_key_bindings
bind -M insert \ec fzf-cd-widget bind -M insert \ec fzf-cd-widget
end end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token' function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# eval is used to do shell expansion on paths # eval is used to do shell expansion on paths
set -l commandline (eval "printf '%s' "(commandline -t)) eval set commandline $commandline
if [ -z $commandline ] if [ -z $commandline ]
# Default to current directory with no --query # Default to current directory with no --query
@@ -138,6 +148,7 @@ function fzf_key_bindings
echo $dir echo $dir
echo $fzf_query echo $fzf_query
echo $prefix
end end
function __fzf_get_dir -d 'Find the longest existing filepath from input string' function __fzf_get_dir -d 'Find the longest existing filepath from input string'

View File

@@ -68,16 +68,6 @@ fzf-file-widget() {
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
# Ensure precmds are run after cd
fzf-redraw-prompt() {
local precmd
for precmd in $precmd_functions; do
$precmd
done
zle reset-prompt
}
zle -N fzf-redraw-prompt
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
@@ -88,16 +78,12 @@ fzf-cd-widget() {
zle redisplay zle redisplay
return 0 return 0
fi fi
if [ -z "$BUFFER" ]; then zle push-line # Clear buffer. Auto-restored on next prompt.
BUFFER="cd ${(q)dir}" BUFFER="cd ${(q)dir}"
zle accept-line zle accept-line
else
print -sr "cd ${(q)dir}"
cd "$dir"
fi
local ret=$? local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion unset dir # ensure this doesn't end up appearing in prompt expansion
zle fzf-redraw-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget

View File

@@ -1,8 +1,6 @@
package fzf package fzf
import ( import (
"bytes"
"regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -82,73 +80,154 @@ func toAnsiString(color tui.Color, offset int) string {
return ret + ";" return ret + ";"
} }
var ansiRegex *regexp.Regexp func isPrint(c uint8) bool {
return '\x20' <= c && c <= '\x7e'
func init() {
/*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- 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
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
} }
func findAnsiStart(str string) int { func matchOperatingSystemCommand(s string) int {
idx := 0 // `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
for ; idx < len(str); idx++ { // ^ match starting here
b := str[idx] //
if b == 0x1b || b == 0x0e || b == 0x0f { i := 5 // prefix matched in nextAnsiEscapeSequence()
return idx for ; i < len(s) && isPrint(s[i]); i++ {
}
if i < len(s) {
if s[i] == '\x07' {
return i + 1
} }
if b == 0x08 && idx > 0 { if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
return idx - 1 return i + 2
} }
} }
return idx return -1
}
func matchControlSequence(s string) int {
// `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
// ^ match starting here
//
i := 2 // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ {
}
if i < len(s) {
c := s[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
return i + 1
}
}
return -1
}
func isCtrlSeqStart(c uint8) bool {
return c == '\\' || c == '[' || c == '(' || c == ')'
}
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used):
//
// "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
//
func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences
i := 0
for ; i < len(s); i++ {
switch s[i] {
case '\x0e', '\x0f', '\x1b', '\x08':
// We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of
// speed and simplicity.
goto Loop
}
}
return -1, -1
Loop:
for ; i < len(s); i++ {
switch s[i] {
case '\x08':
// backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' {
if s[i-1] < utf8.RuneSelf {
return i - 1, i + 1
}
_, n := utf8.DecodeLastRuneInString(s[:i])
return i - n, i + 1
}
case '\x1b':
// match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
if j := matchControlSequence(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
s[i+3] == ';' && isPrint(s[i+4]) {
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b.`
if i+1 < len(s) && s[i+1] != '\n' {
if s[i+1] < utf8.RuneSelf {
return i, i + 2
}
_, n := utf8.DecodeRuneInString(s[i+1:])
return i, i + n + 1
}
case '\x0e', '\x0f':
// match: `[\x0e\x0f]`
return i, i + 1
}
}
return -1, -1
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset // We append to a stack allocated variable that we'll
var output bytes.Buffer // later copy and return, to save on allocations.
offsets := make([]ansiOffset, 0, 32)
if state != nil { if state != nil {
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
} }
prevIdx := 0 var (
runeCount := 0 pstate *ansiState // lazily allocated
output strings.Builder
prevIdx int
runeCount int
)
for idx := 0; idx < len(str); { for idx := 0; idx < len(str); {
idx += findAnsiStart(str[idx:]) // Make sure that we found an ANSI code
if idx == len(str) { start, end := nextAnsiEscapeSequence(str[idx:])
if start == -1 {
break break
} }
start += idx
// Make sure that we found an ANSI code idx += end
offset := ansiRegex.FindStringIndex(str[idx:])
if len(offset) < 2 {
idx++
continue
}
offset[0] += idx
offset[1] += idx
idx = offset[1]
// Check if we should continue // Check if we should continue
prev := str[prevIdx:offset[0]] prev := str[prevIdx:start]
if proc != nil && !proc(prev, state) { if proc != nil && !proc(prev, state) {
return "", nil, nil return "", nil, nil
} }
prevIdx = idx
prevIdx = offset[1] if len(prev) != 0 {
runeCount += utf8.RuneCountInString(prev) runeCount += utf8.RuneCountInString(prev)
output.WriteString(prev) // Grow the buffer size to the maximum possible length (string length
// containing ansi codes) to avoid repetitive allocation
if output.Cap() == 0 {
output.Grow(len(str))
}
output.WriteString(prev)
}
newState := interpretCode(str[offset[0]:offset[1]], state) newState := interpretCode(str[start:idx], state)
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
@@ -157,8 +236,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if newState.colored() { if newState.colored() {
// Append new offset // Append new offset
state = newState if pstate == nil {
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state}) pstate = &ansiState{}
}
*pstate = newState
state = pstate
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
newState,
})
} else { } else {
// Discard state // Discard state
state = nil state = nil
@@ -168,7 +254,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
var rest string var rest string
var trimmed string var trimmed string
if prevIdx == 0 { if prevIdx == 0 {
// No ANSI code found // No ANSI code found
rest = str rest = str
@@ -178,51 +263,75 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(rest) output.WriteString(rest)
trimmed = output.String() trimmed = output.String()
} }
if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if proc != nil { if proc != nil {
proc(rest, state) proc(rest, state)
} }
if len(offsets) == 0 { if len(offsets) > 0 {
return trimmed, nil, state if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
// Return a copy of the offsets slice
a := make([]ansiOffset, len(offsets))
copy(a, offsets)
return trimmed, &a, state
} }
return trimmed, &offsets, state return trimmed, nil, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func parseAnsiCode(s string) (int, string) {
// State var remaining string
var state *ansiState if i := strings.IndexByte(s, ';'); i >= 0 {
remaining = s[i+1:]
s = s[:i]
}
if len(s) > 0 {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, remaining
}
code = code*10 + int(ch)
}
return code, remaining
}
return -1, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
var state ansiState
if prevState == nil { if prevState == nil {
state = &ansiState{-1, -1, 0, -1} state = ansiState{-1, -1, 0, -1}
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg} state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
} }
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if strings.HasSuffix(ansiCode, "0K") { if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
state.lbg = prevState.bg state.lbg = prevState.bg
} }
return state return state
} }
ptr := &state.fg if len(ansiCode) <= 3 {
state256 := 0
init := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.attr = 0 state.attr = 0
state256 = 0 return state
} }
ansiCode = ansiCode[2 : len(ansiCode)-1] ansiCode = ansiCode[2 : len(ansiCode)-1]
if len(ansiCode) == 0 {
init() state256 := 0
} ptr := &state.fg
for _, code := range strings.Split(ansiCode, ";") {
if num, err := strconv.Atoi(code); err == nil { for len(ansiCode) != 0 {
var num int
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
switch state256 { switch state256 {
case 0: case 0:
switch num { switch num {
@@ -253,7 +362,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 24: // tput rmul case 24: // tput rmul
state.attr = state.attr &^ tui.Underline state.attr = state.attr &^ tui.Underline
case 0: case 0:
init() state.fg = -1
state.bg = -1
state.attr = 0
state256 = 0
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
state.fg = tui.Color(num - 30) state.fg = tui.Color(num - 30)
@@ -289,6 +401,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
} }
} }
} }
if state256 > 0 { if state256 > 0 {
*ptr = -1 *ptr = -1
} }

View File

@@ -2,12 +2,190 @@ package fzf
import ( import (
"fmt" "fmt"
"math/rand"
"regexp"
"strings" "strings"
"testing" "testing"
"unicode/utf8"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
) )
// The following regular expression will include not all but most of the
// frequently used ANSI sequences. This regex is used as a reference for
// testing nextAnsiEscapeSequence().
//
// References:
// - https://github.com/gnachman/iTerm2
// - http://ascii-table.com/ansi-escape-sequences.php
// - 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)")
func testParserReference(t testing.TB, str string) {
t.Helper()
toSlice := func(start, end int) []int {
if start == -1 {
return nil
}
return []int{start, end}
}
s := str
for i := 0; ; i++ {
got := toSlice(nextAnsiEscapeSequence(s))
exp := ansiRegexRefence.FindStringIndex(s)
equal := len(got) == len(exp)
if equal {
for i := 0; i < len(got); i++ {
if got[i] != exp[i] {
equal = false
break
}
}
}
if !equal {
var exps, gots []rune
if len(got) == 2 {
gots = []rune(s[got[0]:got[1]])
}
if len(exp) == 2 {
exps = []rune(s[exp[0]:exp[1]])
}
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
return
}
if len(exp) == 0 {
return
}
s = s[exp[1]:]
}
}
func TestNextAnsiEscapeSequence(t *testing.T) {
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
"\x1b椙",
"椙\x08",
"\n\x08",
"X\x08",
"",
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
ansiBenchmarkString,
}
for _, s := range testStrs {
testParserReference(t, s)
}
}
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
ansiBenchmarkString,
}
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
modifyString := func(s string, rr *rand.Rand) string {
n := rr.Intn(len(s))
b := []rune(s)
for ; n >= 0 && len(b) != 0; n-- {
i := rr.Intn(len(b))
switch x := rr.Intn(4); x {
case 0:
b = append(b[:i], b[i+1:]...)
case 1:
j := rr.Intn(len(replacementBytes) - 1)
b[i] = replacementBytes[j]
case 2:
x := rune(rr.Intn(utf8.MaxRune))
for !utf8.ValidRune(x) {
x = rune(rr.Intn(utf8.MaxRune))
}
b[i] = x
case 3:
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
default:
t.Fatalf("unsupported value: %d", x)
}
}
return string(b)
}
rr := rand.New(rand.NewSource(1))
for _, s := range testStrs {
for i := 1_000; i >= 0; i-- {
testParserReference(t, modifyString(s, rr))
}
}
}
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
randomString := func(rr *rand.Rand) string {
numChars := rand.Intn(50)
codePoints := make([]rune, numChars)
for i := 0; i < len(codePoints); i++ {
var r rune
for n := 0; n < 1000; n++ {
r = rune(rr.Intn(utf8.MaxRune))
// Allow 10% of runes to be invalid
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
break
}
}
codePoints[i] = r
}
return string(codePoints)
}
rr := rand.New(rand.NewSource(1))
for i := 0; i < 100_000; i++ {
testParserReference(t, randomString(rr))
}
}
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr tui.Attr var attr tui.Attr
@@ -185,3 +363,64 @@ func TestAnsiCodeStringConversion(t *testing.T) {
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1}, &ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m") "\x1b[2;3;7;38;2;10;20;30;48;5;100m")
} }
func TestParseAnsiCode(t *testing.T) {
tests := []struct {
In, Exp string
N int
}{
{"123", "", 123},
{"1a", "", -1},
{"1a;12", "12", -1},
{"12;a", "a", 12},
{"-2", "", -1},
}
for _, x := range tests {
n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
}
}
}
// kernel/bpf/preload/iterators/README
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
_, o := nextAnsiEscapeSequence(s)
if o == -1 {
break
}
s = s[o:]
}
}
}
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
// previously used regex based implementation.
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
a := ansiRegexRefence.FindStringIndex(s)
if len(a) == 0 {
break
}
s = s[a[1]:]
}
}
}
func BenchmarkExtractColor(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
extractColor(ansiBenchmarkString, nil, nil)
}
}

View File

@@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
cache := NewChunkCache() cache := NewChunkCache()
chunk1p := &Chunk{} chunk1p := &Chunk{}
chunk2p := &Chunk{count: chunkSize} chunk2p := &Chunk{count: chunkSize}
items1 := []Result{Result{}} items1 := []Result{{}}
items2 := []Result{Result{}, Result{}} items2 := []Result{{}, {}}
cache.Add(chunk1p, "foo", items1) cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2) cache.Add(chunk2p, "bar", items2)

View File

@@ -73,6 +73,7 @@ const (
EvtSearchFin EvtSearchFin
EvtHeader EvtHeader
EvtReady EvtReady
EvtQuit
) )
const ( const (

View File

@@ -254,7 +254,11 @@ func Run(opts *Options, version string, revision string) {
} }
for evt, value := range *events { for evt, value := range *events {
switch evt { switch evt {
case EvtQuit:
if reading {
reader.terminate()
}
os.Exit(value.(int))
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand) restart(*nextCommand)

View File

@@ -3,7 +3,6 @@ package fzf
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/user"
"runtime" "runtime"
"testing" "testing"
) )
@@ -12,16 +11,12 @@ func TestHistory(t *testing.T) {
maxHistory := 50 maxHistory := 50
// Invalid arguments // Invalid arguments
user, _ := user.Current()
var paths []string var paths []string
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// GOPATH should exist, so we shouldn't be able to override it // GOPATH should exist, so we shouldn't be able to override it
paths = []string{os.Getenv("GOPATH")} paths = []string{os.Getenv("GOPATH")}
} else { } else {
paths = []string{"/etc", "/proc"} paths = []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
} }
for _, path := range paths { for _, path := range paths {

View File

@@ -106,7 +106,6 @@ func (mg *Merger) mergedGet(idx int) Result {
minIdx = listIdx minIdx = listIdx
} }
} }
mg.cursors[listIdx] = cursor
} }
if minIdx >= 0 { if minIdx >= 0 {

View File

@@ -58,7 +58,7 @@ const usage = `usage: fzf [options]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list] --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|horizontal|vertical| [rounded|sharp|horizontal|vertical|
top|bottom|left|right] (default: rounded) top|bottom|left|right|none] (default: rounded)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden] --info=STYLE Finder info style [default|inline|hidden]
@@ -81,11 +81,11 @@ const usage = `usage: fzf [options]
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]] [up|down|left|right][,SIZE[%]]
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden] [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
[:rounded|sharp|noborder] [,border-BORDER_OPT]
[:+SCROLL[-OFFSET]] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[:default] [,default]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -161,15 +161,16 @@ const (
) )
type previewOpts struct { type previewOpts struct {
command string command string
position windowPosition position windowPosition
size sizeSpec size sizeSpec
scroll string scroll string
hidden bool hidden bool
wrap bool wrap bool
cycle bool cycle bool
follow bool follow bool
border tui.BorderShape border tui.BorderShape
headerLines int
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@@ -231,7 +232,7 @@ type Options struct {
} }
func defaultPreviewOpts(command string) previewOpts { func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded} return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
} }
func defaultOptions() *Options { func defaultOptions() *Options {
@@ -435,11 +436,13 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderLeft return tui.BorderLeft
case "right": case "right":
return tui.BorderRight return tui.BorderRight
case "none":
return tui.BorderNone
default: default:
if optional && str == "" { if optional && str == "" {
return tui.BorderRounded return tui.BorderRounded
} }
errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right)") errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right|none)")
} }
return tui.BorderNone return tui.BorderNone
} }
@@ -449,7 +452,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
errorExit(message) errorExit(message)
} }
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string(escapedComma)) str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
tokens := strings.Split(str, ",") tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") { if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
tokens = append(tokens, ",") tokens = append(tokens, ",")
@@ -460,7 +463,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
if len(key) == 0 { if len(key) == 0 {
continue // ignore continue // ignore
} }
key = strings.ReplaceAll(key, string(escapedComma), ",") key = strings.ReplaceAll(key, string([]rune{escapedComma}), ",")
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
add := func(e tui.EventType) { add := func(e tui.EventType) {
chords[e.AsEvent()] = key chords[e.AsEvent()] = key
@@ -745,7 +748,7 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[tui.Event][]action, str string) { func parseKeymap(keymap map[tui.Event][]action, str string) {
@@ -759,6 +762,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
prefix = symbol + "reload" prefix = symbol + "reload"
} else if strings.HasPrefix(src[1:], "preview") { } else if strings.HasPrefix(src[1:], "preview") {
prefix = symbol + "preview" prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") {
prefix = symbol + "unbind"
} else if strings.HasPrefix(src[1:], "change-prompt") { } else if strings.HasPrefix(src[1:], "change-prompt") {
prefix = symbol + "change-prompt" prefix = symbol + "change-prompt"
} else if src[len(prefix)] == '-' { } else if src[len(prefix)] == '-' {
@@ -839,6 +844,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actDeleteChar) appendAction(actDeleteChar)
case "delete-char/eof": case "delete-char/eof":
appendAction(actDeleteCharEOF) appendAction(actDeleteCharEOF)
case "deselect":
appendAction(actDeselect)
case "end-of-line": case "end-of-line":
appendAction(actEndOfLine) appendAction(actEndOfLine)
case "cancel": case "cancel":
@@ -879,10 +886,14 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actToggleAll) appendAction(actToggleAll)
case "toggle-search": case "toggle-search":
appendAction(actToggleSearch) appendAction(actToggleSearch)
case "select":
appendAction(actSelect)
case "select-all": case "select-all":
appendAction(actSelectAll) appendAction(actSelectAll)
case "deselect-all": case "deselect-all":
appendAction(actDeselectAll) appendAction(actDeselectAll)
case "close":
appendAction(actClose)
case "toggle": case "toggle":
appendAction(actToggle) appendAction(actToggle)
case "down": case "down":
@@ -948,6 +959,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
offset = len("preview") offset = len("preview")
case actChangePrompt: case actChangePrompt:
offset = len("change-prompt") offset = len("change-prompt")
case actUnbind:
offset = len("unbind")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@@ -955,15 +968,21 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
default: default:
offset = len("execute") offset = len("execute")
} }
var actionArg string
if spec[offset] == ':' { if spec[offset] == ':' {
if specIndex == len(specs)-1 { if specIndex == len(specs)-1 {
actions = append(actions, action{t: t, a: spec[offset+1:]}) actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg})
} else { } else {
prevSpec = spec + "+" prevSpec = spec + "+"
continue continue
} }
} else { } else {
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]}) actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, action{t: t, a: actionArg})
}
if t == actUnbind {
parseKeyChords(actionArg, "unbind target required")
} }
} }
} }
@@ -985,6 +1004,8 @@ func isExecuteAction(str string) actionType {
switch prefix { switch prefix {
case "reload": case "reload":
return actReload return actReload
case "unbind":
return actUnbind
case "preview": case "preview":
return actPreview return actPreview
case "change-prompt": case "change-prompt":
@@ -1069,9 +1090,11 @@ func parseInfoStyle(str string) infoStyle {
} }
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
tokens := strings.Split(input, ":") delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility
sizeRegex := regexp.MustCompile("^[0-9]+%?$") sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$") offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
tokens := delimRegex.Split(input, -1)
for _, token := range tokens { for _, token := range tokens {
switch token { switch token {
case "": case "":
@@ -1097,21 +1120,35 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.position = posLeft opts.position = posLeft
case "right": case "right":
opts.position = posRight opts.position = posRight
case "rounded", "border": case "rounded", "border", "border-rounded":
opts.border = tui.BorderRounded opts.border = tui.BorderRounded
case "sharp": case "sharp", "border-sharp":
opts.border = tui.BorderSharp opts.border = tui.BorderSharp
case "noborder": case "noborder", "border-none":
opts.border = tui.BorderNone opts.border = tui.BorderNone
case "border-horizontal":
opts.border = tui.BorderHorizontal
case "border-vertical":
opts.border = tui.BorderVertical
case "border-top":
opts.border = tui.BorderTop
case "border-bottom":
opts.border = tui.BorderBottom
case "border-left":
opts.border = tui.BorderLeft
case "border-right":
opts.border = tui.BorderRight
case "follow": case "follow":
opts.follow = true opts.follow = true
case "nofollow": case "nofollow":
opts.follow = false opts.follow = false
default: default:
if sizeRegex.MatchString(token) { if headerRegex.MatchString(token) {
opts.headerLines = atoi(token[1:])
} else if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size") opts.size = parseSize(token, 99, "window size")
} else if offsetRegex.MatchString(token) { } else if offsetRegex.MatchString(token) {
opts.scroll = token[1:] opts.scroll = token
} else { } else {
errorExit("invalid preview window option: " + token) errorExit("invalid preview window option: " + token)
} }
@@ -1358,7 +1395,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
case "--height": case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height": case "--min-height":
@@ -1511,15 +1548,13 @@ func validateSign(sign string, signOptName string) error {
if sign == "" { if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName) return fmt.Errorf("%v cannot be empty", signOptName)
} }
widthSum := 0
for _, r := range sign { for _, r := range sign {
if !unicode.IsGraphic(r) { if !unicode.IsGraphic(r) {
return fmt.Errorf("invalid character in %v", signOptName) return fmt.Errorf("invalid character in %v", signOptName)
} }
widthSum += runewidth.RuneWidth(r) }
if widthSum > 2 { if runewidth.StringWidth(sign) > 2 {
return fmt.Errorf("%v display width should be up to 2", signOptName) return fmt.Errorf("%v display width should be up to 2", signOptName)
}
} }
return nil return nil
} }

View File

@@ -102,7 +102,7 @@ func TestIrrelevantNth(t *testing.T) {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
} }
} }
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
@@ -384,23 +384,23 @@ func TestPreviewOpts(t *testing.T) {
opts.Preview.size.size == 50) { opts.Preview.size.size == 50) {
t.Error() t.Error()
} }
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap:+{1}-/2") opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
if !(opts.Preview.command == "cat {}" && if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true && opts.Preview.hidden == true &&
opts.Preview.wrap == true && opts.Preview.wrap == true &&
opts.Preview.position == posLeft && opts.Preview.position == posLeft &&
opts.Preview.scroll == "{1}-/2" && opts.Preview.scroll == "+{1}-/2" &&
opts.Preview.size.percent == false && opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) { opts.Preview.size.size == 15) {
t.Error(opts.Preview) t.Error(opts.Preview)
} }
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle") opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
if !(opts.Preview.command == "" && if !(opts.Preview.command == "" &&
opts.Preview.hidden == true && opts.Preview.hidden == true &&
opts.Preview.wrap == true && opts.Preview.wrap == true &&
opts.Preview.cycle == true && opts.Preview.cycle == true &&
opts.Preview.position == posDown && opts.Preview.position == posDown &&
opts.Preview.scroll == "{1}-/2" && opts.Preview.scroll == "+{1}+3-1-2/2" &&
opts.Preview.size.percent == false && opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) { opts.Preview.size.size == 15) {
t.Error(opts.Preview.size.size) t.Error(opts.Preview.size.size)

View File

@@ -337,7 +337,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
var input []Token var input []Token
if len(p.nth) == 0 { if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}} input = []Token{{text: &item.text, prefixLength: 0}}
} else { } else {
input = p.transformInput(item) input = p.transformInput(item)
} }
@@ -350,7 +350,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
var input []Token var input []Token
if len(p.nth) == 0 { if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}} input = []Token{{text: &item.text, prefixLength: 0}}
} else { } else {
input = p.transformInput(item) input = p.transformInput(item)
} }

View File

@@ -131,7 +131,7 @@ func TestCaseSensitivity(t *testing.T) {
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{{1, 1}})
origBytes := []byte("junegunn.choi") origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} { for _, extended := range []bool{false, true} {

View File

@@ -164,7 +164,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
// combination of either [hl and bg] or [hl+ and bg+]. // combination of either [hl and bg] or [hl+ and bg+].
// //
// If the original text already has background color, and the // If the original text already has background color, and the
// forground color of colMatch is -1, we shouldn't only apply the // foreground color of colMatch is -1, we shouldn't only apply the
// background color of colMatch. // background color of colMatch.
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline // e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline // echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline

View File

@@ -18,8 +18,8 @@ func withIndex(i *Item, index int) *Item {
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
offsets := []Offset{ offsets := []Offset{
Offset{3, 5}, Offset{2, 7}, {3, 5}, {2, 7},
Offset{1, 3}, Offset{2, 9}} {1, 3}, {2, 9}}
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 || if offsets[0][0] != 1 || offsets[0][1] != 3 ||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
// Sort by relevance // Sort by relevance
item3 := buildResult( item3 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3) withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
item4 := buildResult( item4 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4) withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
item5 := buildResult( item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5) withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
item6 := buildResult( item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6) withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
items = []Result{item1, item2, item3, item4, item5, item6} items = []Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 && if !(items[0] == item6 && items[1] == item5 &&

View File

@@ -2,7 +2,6 @@ package fzf
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -15,6 +14,9 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -22,8 +24,9 @@ import (
// import "github.com/pkg/profile" // import "github.com/pkg/profile"
var placeholder *regexp.Regexp var placeholder *regexp.Regexp
var numericPrefix *regexp.Regexp
var whiteSuffix *regexp.Regexp var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string var activeTempFiles []string
const ellipsis string = ".." const ellipsis string = ".."
@@ -31,8 +34,9 @@ const clearCode string = "\x1b[2J"
func init() { func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`)
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{} activeTempFiles = []string{}
} }
@@ -133,6 +137,7 @@ type Terminal struct {
count int count int
progress int progress int
reading bool reading bool
running bool
failed *string failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
@@ -157,6 +162,7 @@ type Terminal struct {
slab *util.Slab slab *util.Slab
theme *tui.ColorTheme theme *tui.ColorTheme
tui tui.Renderer tui tui.Renderer
executing *util.AtomicBool
} }
type selectedItem struct { type selectedItem struct {
@@ -221,6 +227,7 @@ const (
actClearScreen actClearScreen
actClearQuery actClearQuery
actClearSelection actClearSelection
actClose
actDeleteChar actDeleteChar
actDeleteCharEOF actDeleteCharEOF
actEndOfLine actEndOfLine
@@ -275,6 +282,9 @@ const (
actReload actReload
actDisableSearch actDisableSearch
actEnableSearch actEnableSearch
actSelect
actDeselect
actUnbind
) )
type placeholderFlags struct { type placeholderFlags struct {
@@ -501,6 +511,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
running: true,
failed: nil, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
@@ -522,7 +533,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
startChan: make(chan bool, 1), startChan: make(chan bool, 1),
killChan: make(chan int), killChan: make(chan int),
tui: renderer, tui: renderer,
initFunc: func() { renderer.Init() }} initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -664,11 +676,8 @@ func (t *Terminal) sortSelected() []selectedItem {
} }
func (t *Terminal) displayWidth(runes []rune) int { func (t *Terminal) displayWidth(runes []rune) int {
l := 0 width, _ := util.RunesWidth(runes, 0, t.tabstop, 0)
for _, r := range runes { return width
l += util.RuneWidth(r, l, t.tabstop)
}
return l
} }
const ( const (
@@ -822,16 +831,33 @@ func (t *Terminal) resizeWindows() {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
pwidth := w pwidth := w
pheight := h pheight := h
if t.previewOpts.border != tui.BorderNone { var previewBorder tui.BorderStyle
previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode) if t.previewOpts.border == tui.BorderNone {
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) previewBorder = tui.MakeTransparentBorder()
} else {
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
}
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
switch t.previewOpts.border {
case tui.BorderSharp, tui.BorderRounded:
pwidth -= 4 pwidth -= 4
pheight -= 2 pheight -= 2
x += 2 x += 2
y += 1 y += 1
} else { case tui.BorderLeft:
previewBorder := tui.MakeTransparentBorder() pwidth -= 2
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) x += 2
case tui.BorderRight:
pwidth -= 2
case tui.BorderTop:
pheight -= 1
y += 1
case tui.BorderBottom:
pheight -= 1
case tui.BorderHorizontal:
pheight -= 2
y += 1
case tui.BorderVertical:
pwidth -= 4 pwidth -= 4
x += 2 x += 2
} }
@@ -839,9 +865,13 @@ func (t *Terminal) resizeWindows() {
} }
verticalPad := 2 verticalPad := 2
minPreviewHeight := 3 minPreviewHeight := 3
if t.previewOpts.border == tui.BorderNone { switch t.previewOpts.border {
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
verticalPad = 0 verticalPad = 0
minPreviewHeight = 1 minPreviewHeight = 1
case tui.BorderTop, tui.BorderBottom:
verticalPad = 1
minPreviewHeight = 2
} }
switch t.previewOpts.position { switch t.previewOpts.position {
case posUp: case posUp:
@@ -1111,36 +1141,26 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
t.prevLines[i] = newLine t.prevLines[i] = newLine
} }
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
// We start from the beginning to handle tab characters // We start from the beginning to handle tab characters
l := 0 width, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
for idx, r := range runes { if overflowIdx >= 0 {
l += util.RuneWidth(r, l, t.tabstop) return runes[:overflowIdx], true
if l > width {
return runes[:idx], len(runes) - idx
}
} }
return runes, 0 return runes, false
} }
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0 width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
for _, r := range runes { return width
l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
if l > limit {
// Early exit
return l
}
}
return l
} }
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
width = util.Max(0, width) width = util.Max(0, width)
var trimmed int32 var trimmed int32
// Assume that each rune takes at least one column on screen // Assume that each rune takes at least one column on screen
if len(runes) > width { if len(runes) > width+2 {
diff := len(runes) - width diff := len(runes) - width - 2
trimmed = int32(diff) trimmed = int32(diff)
runes = runes[diff:] runes = runes[diff:]
} }
@@ -1288,18 +1308,37 @@ func (t *Terminal) renderPreviewSpinner() {
} }
} }
func (t *Terminal) renderPreviewText(unchanged bool) { func (t *Terminal) renderPreviewArea(unchanged bool) {
maxWidth := t.pwindow.Width()
lineNo := -t.previewer.offset
height := t.pwindow.Height()
if unchanged { if unchanged {
t.pwindow.MoveAndClear(0, 0) t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else { } else {
t.previewed.filled = false t.previewed.filled = false
t.pwindow.Erase() t.pwindow.Erase()
} }
height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines
headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) {
header = t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
if !unchanged {
t.pwindow.FinishFill()
}
}
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState var ansi *ansiState
for _, line := range t.previewer.lines { for _, line := range lines {
var lbg tui.Color = -1 var lbg tui.Color = -1
if ansi != nil { if ansi != nil {
ansi.lbg = -1 ansi.lbg = -1
@@ -1313,8 +1352,9 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
prefixWidth := 0 prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str) trimmed := []rune(str)
isTrimmed := false
if !t.previewOpts.wrap { if !t.previewOpts.wrap {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
} }
str, width := t.processTabs(trimmed, prefixWidth) str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width prefixWidth += width
@@ -1324,7 +1364,8 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
} else { } else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str) fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
} }
return fillRet == tui.FillContinue return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
}) })
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
if fillRet == tui.FillNextLine { if fillRet == tui.FillNextLine {
@@ -1345,9 +1386,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
} }
lineNo++ lineNo++
} }
if !unchanged {
t.pwindow.FinishFill()
}
} }
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
@@ -1360,7 +1398,7 @@ func (t *Terminal) printPreview() {
t.previewer.version == t.previewed.version && t.previewer.version == t.previewed.version &&
t.previewer.offset == t.previewed.offset t.previewer.offset == t.previewed.offset
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
t.renderPreviewText(unchanged) t.renderPreviewArea(unchanged)
t.renderPreviewSpinner() t.renderPreviewSpinner()
t.previewed.numLines = numLines t.previewed.numLines = numLines
t.previewed.version = t.previewer.version t.previewed.version = t.previewer.version
@@ -1373,7 +1411,7 @@ func (t *Terminal) printPreviewDelayed() {
} }
t.previewer.scrollable = false t.previewer.scrollable = false
t.renderPreviewText(true) t.renderPreviewArea(true)
message := t.trimMessage("Loading ..", t.pwindow.Width()) message := t.trimMessage("Loading ..", t.pwindow.Width())
pos := t.pwindow.Width() - len(message) pos := t.pwindow.Width() - len(message)
@@ -1382,16 +1420,21 @@ func (t *Terminal) printPreviewDelayed() {
} }
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer var strbuf strings.Builder
l := prefixWidth l := prefixWidth
for _, r := range runes { gr := uniseg.NewGraphemes(string(runes))
w := util.RuneWidth(r, l, t.tabstop) for gr.Next() {
l += w rs := gr.Runes()
if r == '\t' { str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = t.tabstop - l%t.tabstop
strbuf.WriteString(strings.Repeat(" ", w)) strbuf.WriteString(strings.Repeat(" ", w))
} else { } else {
strbuf.WriteRune(r) w = runewidth.StringWidth(str)
strbuf.WriteString(str)
} }
l += w
} }
return strbuf.String(), l return strbuf.String(), l
} }
@@ -1566,43 +1609,33 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
} }
// Ascii to positive integer
func atopi(s string) int {
matches := numericPrefix.FindStringSubmatch(s)
if len(matches) < 2 {
return 0
}
n, e := strconv.Atoi(matches[1])
if e != nil || n < 1 {
return 0
}
return n
}
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int { func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list) offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
nums := strings.Split(offsetExpr, "-") t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
switch len(nums) {
case 0: atoi := func(s string) int {
return 0 n, e := strconv.Atoi(s)
case 1, 2: if e != nil {
base := atopi(nums[0])
if base == 0 {
return 0 return 0
} else if len(nums) == 1 {
return base - 1
} }
if nums[1][0] == '/' { return n
denom := atopi(nums[1][1:]) }
base := -1
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
if denom == 0 { if denom == 0 {
return base return base
} }
return base - height/denom return base - height/denom
} }
return base - atopi(nums[1]) - 1 base += atoi(component)
default:
return 0
} }
return base
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1705,8 +1738,9 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} }
command := t.replacePlaceholder(template, forcePlus, string(t.input), list) command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false) cmd := util.ExecCommand(command, false)
t.executing.Set(true)
if !background { if !background {
cmd.Stdin = os.Stdin cmd.Stdin = tui.TtyIn()
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
t.tui.Pause(true) t.tui.Pause(true)
@@ -1719,6 +1753,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run() cmd.Run()
t.tui.Resume(false, false) t.tui.Resume(false, false)
} }
t.executing.Set(false)
cleanTemporaryFiles() cleanTemporaryFiles()
} }
@@ -1784,11 +1819,26 @@ func (t *Terminal) selectItem(item *Item) bool {
return true return true
} }
func (t *Terminal) selectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
return false
}
return t.selectItem(item)
}
func (t *Terminal) deselectItem(item *Item) { func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index()) delete(t.selected, item.Index())
t.version++ t.version++
} }
func (t *Terminal) deselectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
t.deselectItem(item)
return true
}
return false
}
func (t *Terminal) toggleItem(item *Item) bool { func (t *Terminal) toggleItem(item *Item) bool {
if _, found := t.selected[item.Index()]; !found { if _, found := t.selected[item.Index()]; !found {
return t.selectItem(item) return t.selectItem(item)
@@ -1802,7 +1852,7 @@ func (t *Terminal) killPreview(code int) {
case t.killChan <- code: case t.killChan <- code:
default: default:
if code != exitCancel { if code != exitCancel {
os.Exit(code) t.eventBox.Set(EvtQuit, code)
} }
} }
} }
@@ -1819,8 +1869,12 @@ func (t *Terminal) Loop() {
intChan := make(chan os.Signal, 1) intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
<-intChan for s := range intChan {
t.reqBox.Set(reqQuit, nil) // Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
if !(s == os.Interrupt && t.executing.Get()) {
t.reqBox.Set(reqQuit, nil)
}
}
}() }()
contChan := make(chan os.Signal, 1) contChan := make(chan os.Signal, 1)
@@ -1897,7 +1951,7 @@ func (t *Terminal) Loop() {
cmd := util.ExecCommand(command, true) cmd := util.ExecCommand(command, true)
if pwindow != nil { if pwindow != nil {
height := pwindow.Height() height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height)) initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ() env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height) lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -1930,6 +1984,7 @@ func (t *Terminal) Loop() {
}() }()
// Goroutine 2 periodically requests rendering // Goroutine 2 periodically requests rendering
rendered := util.NewAtomicBool(false)
go func(version int64) { go func(version int64) {
lines := []string{} lines := []string{}
spinner := makeSpinner(t.unicode) spinner := makeSpinner(t.unicode)
@@ -1944,6 +1999,7 @@ func (t *Terminal) Loop() {
if spinnerIndex >= 0 { if spinnerIndex >= 0 {
spin := spinner[spinnerIndex%len(spinner)] spin := spinner[spinnerIndex%len(spinner)]
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin}) t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
rendered.Set(true)
offset = -1 offset = -1
} }
spinnerIndex++ spinnerIndex++
@@ -1963,6 +2019,7 @@ func (t *Terminal) Loop() {
} }
if err != nil { if err != nil {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""}) t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
rendered.Set(true)
break Loop break Loop
} }
} }
@@ -1982,9 +2039,15 @@ func (t *Terminal) Loop() {
case code := <-t.killChan: case code := <-t.killChan:
if code != exitCancel { if code != exitCancel {
util.KillCommand(cmd) util.KillCommand(cmd)
os.Exit(code) t.eventBox.Set(EvtQuit, code)
} else { } else {
timer := time.NewTimer(previewCancelWait) // We can immediately kill a long-running preview program
// once we started rendering its partial output
delay := previewCancelWait
if rendered.Get() {
delay = 0
}
timer := time.NewTimer(delay)
select { select {
case <-timer.C: case <-timer.C:
util.KillCommand(cmd) util.KillCommand(cmd)
@@ -2019,16 +2082,6 @@ func (t *Terminal) Loop() {
}() }()
} }
exit := func(getCode func() int) {
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
// prof.Stop()
t.killPreview(code)
}
refreshPreview := func(command string) { refreshPreview := func(command string) {
if len(command) > 0 && t.isPreviewEnabled() { if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
@@ -2040,7 +2093,19 @@ func (t *Terminal) Loop() {
go func() { go func() {
var focusedIndex int32 = minItem.Index() var focusedIndex int32 = minItem.Index()
var version int64 = -1 var version int64 = -1
for { running := true
code := exitError
exit := func(getCode func() int) {
t.tui.Close()
code = getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
running = false
t.mutex.Unlock()
}
for running {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
t.mutex.Lock() t.mutex.Lock()
@@ -2086,6 +2151,7 @@ func (t *Terminal) Loop() {
} }
return exitNoMatch return exitNoMatch
}) })
return
case reqPreviewDisplay: case reqPreviewDisplay:
result := value.(previewResult) result := value.(previewResult)
if t.previewer.version != result.version { if t.previewer.version != result.version {
@@ -2097,7 +2163,7 @@ func (t *Terminal) Loop() {
if t.previewer.following { if t.previewer.following {
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height() t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
} else if result.offset >= 0 { } else if result.offset >= 0 {
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1) t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
} }
t.printPreview() t.printPreview()
case reqPreviewRefresh: case reqPreviewRefresh:
@@ -2110,14 +2176,18 @@ func (t *Terminal) Loop() {
t.printer(string(t.input)) t.printer(string(t.input))
return exitOk return exitOk
}) })
return
case reqQuit: case reqQuit:
exit(func() int { return exitInterrupt }) exit(func() int { return exitInterrupt })
return
} }
} }
t.refresh() t.refresh()
t.mutex.Unlock() t.mutex.Unlock()
}) })
} }
// prof.Stop()
t.killPreview(code)
}() }()
looping := true looping := true
@@ -2166,7 +2236,7 @@ func (t *Terminal) Loop() {
if t.previewOpts.cycle { if t.previewOpts.cycle {
newOffset = (newOffset + numLines) % numLines newOffset = (newOffset + numLines) % numLines
} }
newOffset = util.Constrain(newOffset, 0, numLines-1) newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
if t.previewer.offset != newOffset { if t.previewer.offset != newOffset {
t.previewer.offset = newOffset t.previewer.offset = newOffset
req(reqPreviewRefresh) req(reqPreviewRefresh)
@@ -2222,6 +2292,8 @@ func (t *Terminal) Loop() {
case actTogglePreviewWrap: case actTogglePreviewWrap:
if t.hasPreviewWindow() { if t.hasPreviewWindow() {
t.previewOpts.wrap = !t.previewOpts.wrap t.previewOpts.wrap = !t.previewOpts.wrap
// Reset preview version so that full redraw occurs
t.previewed.version = 0
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actToggleSort: case actToggleSort:
@@ -2332,6 +2404,22 @@ func (t *Terminal) Loop() {
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }
case actClose:
if t.isPreviewEnabled() {
togglePreview(false)
} else {
req(reqQuit)
}
case actSelect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
req(reqList, reqInfo)
}
case actDeselect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
req(reqList, reqInfo)
}
case actToggle: case actToggle:
if t.multi > 0 && t.merger.Length() > 0 && toggle() { if t.multi > 0 && t.merger.Length() > 0 && toggle() {
req(reqList) req(reqList)
@@ -2570,6 +2658,11 @@ func (t *Terminal) Loop() {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command newCommand = &command
} }
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
delete(t.keymap, key)
}
} }
return true return true
} }
@@ -2632,18 +2725,16 @@ func (t *Terminal) Loop() {
} }
func (t *Terminal) constrain() { func (t *Terminal) constrain() {
// count of items to display allowed by filtering
count := t.merger.Length() count := t.merger.Length()
// count of lines can be displayed
height := t.maxItems() height := t.maxItems()
diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1) t.cy = util.Constrain(t.cy, 0, count-1)
t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy)
// Adjustment minOffset := t.cy - height + 1
if count-t.offset < height { maxOffset := util.Max(util.Min(count-height, t.cy), 0)
t.offset = util.Max(0, count-height) t.offset = util.Constrain(t.offset, minOffset, maxOffset)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
}
t.offset = util.Max(0, t.offset)
} }
func (t *Terminal) vmove(o int, allowCycle bool) { func (t *Terminal) vmove(o int, allowCycle bool) {

View File

@@ -35,7 +35,7 @@ type Delimiter struct {
str *string str *string
} }
// String returns the string representation of a Delimeter. // String returns the string representation of a Delimiter.
func (d Delimiter) String() string { func (d Delimiter) String() string {
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
} }

View File

@@ -10,9 +10,10 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/util" "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
const ( const (
@@ -50,7 +51,7 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
} }
bytes = bytes[sz:] bytes = bytes[sz:]
} }
r.queued += string(runes) r.queued.WriteString(string(runes))
} }
func (r *LightRenderer) csi(code string) { func (r *LightRenderer) csi(code string) {
@@ -58,9 +59,9 @@ func (r *LightRenderer) csi(code string) {
} }
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if len(r.queued) > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, r.queued) fmt.Fprint(os.Stderr, r.queued.String())
r.queued = "" r.queued.Reset()
} }
} }
@@ -74,7 +75,7 @@ type LightRenderer struct {
clickY []int clickY []int
ttyin *os.File ttyin *os.File
buffer []byte buffer []byte
origState *terminal.State origState *term.State
width int width int
height int height int
yoffset int yoffset int
@@ -82,7 +83,7 @@ type LightRenderer struct {
escDelay int escDelay int
fullscreen bool fullscreen bool
upOneLine bool upOneLine bool
queued string queued strings.Builder
y int y int
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
@@ -693,13 +694,17 @@ func (w *LightWindow) drawBorder() {
} }
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) w.CPrint(color, repeat(w.border.horizontal, w.width))
} }
if bottom { if bottom {
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width)) w.CPrint(color, repeat(w.border.horizontal, w.width))
} }
} }
@@ -708,14 +713,18 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
if !left || !right { if !left || !right {
width++ width++
} }
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0) w.Move(y, 0)
if left { if left {
w.CPrint(ColBorder, string(w.border.vertical)) w.CPrint(color, string(w.border.vertical))
} }
w.CPrint(ColBorder, repeat(' ', width)) w.CPrint(color, repeat(' ', width))
if right { if right {
w.CPrint(ColBorder, string(w.border.vertical)) w.CPrint(color, string(w.border.vertical))
} }
} }
} }
@@ -881,20 +890,26 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
lines := []wrappedLine{} lines := []wrappedLine{}
width := 0 width := 0
line := "" line := ""
for _, r := range input { gr := uniseg.NewGraphemes(input)
w := util.RuneWidth(r, prefixLength+width, 8) for gr.Next() {
width += w rs := gr.Runes()
str := string(r) str := string(rs)
if r == '\t' { var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w) str = repeat(' ', w)
} else {
w = runewidth.StringWidth(str)
} }
width += w
if prefixLength+width <= max { if prefixLength+width <= max {
line += str line += str
} else { } else {
lines = append(lines, wrappedLine{string(line), width - w}) lines = append(lines, wrappedLine{string(line), width - w})
line = str line = str
prefixLength = 0 prefixLength = 0
width = util.RuneWidth(r, prefixLength, 8) width = w
} }
} }
lines = append(lines, wrappedLine{string(line), width}) lines = append(lines, wrappedLine{string(line), width})
@@ -906,12 +921,6 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines { for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 {
w.Move(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false) w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth w.posx += wl.displayWidth
@@ -926,6 +935,14 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
} }
} }
} }
if w.posx+1 >= w.Width() {
if w.posy+1 >= w.height {
return FillSuspend
}
w.Move(w.posy+1, 0)
onMove()
return FillNextLine
}
return FillContinue return FillContinue
} }

View File

@@ -10,7 +10,7 @@ import (
"syscall" "syscall"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/term"
) )
func IsLightRendererSupported() bool { func IsLightRendererSupported() bool {
@@ -34,12 +34,12 @@ func (r *LightRenderer) fd() int {
func (r *LightRenderer) initPlatform() error { func (r *LightRenderer) initPlatform() error {
fd := r.fd() fd := r.fd()
origState, err := terminal.GetState(fd) origState, err := term.GetState(fd)
if err != nil { if err != nil {
return err return err
} }
r.origState = origState r.origState = origState
terminal.MakeRaw(fd) term.MakeRaw(fd)
return nil return nil
} }
@@ -63,15 +63,15 @@ func openTtyIn() *os.File {
} }
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {
terminal.MakeRaw(r.fd()) term.MakeRaw(r.fd())
} }
func (r *LightRenderer) restoreTerminal() { func (r *LightRenderer) restoreTerminal() {
terminal.Restore(r.fd(), r.origState) term.Restore(r.fd(), r.origState)
} }
func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) updateTerminalSize() {
width, height, err := terminal.GetSize(r.fd()) width, height, err := term.GetSize(r.fd())
if err == nil { if err == nil {
r.width = width r.width = width

View File

@@ -5,11 +5,16 @@ package tui
import ( import (
"os" "os"
"syscall" "syscall"
"time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
const (
timeoutInterval = 10
)
var ( var (
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
@@ -60,7 +65,7 @@ func (r *LightRenderer) initPlatform() error {
// channel for non-blocking reads. Buffer to make sure // channel for non-blocking reads. Buffer to make sure
// we get the ESC sets: // we get the ESC sets:
r.ttyinChannel = make(chan byte, 12) r.ttyinChannel = make(chan byte, 1024)
// the following allows for non-blocking IO. // the following allows for non-blocking IO.
// syscall.SetNonblock() is a NOOP under Windows. // syscall.SetNonblock() is a NOOP under Windows.
@@ -68,9 +73,6 @@ func (r *LightRenderer) initPlatform() error {
fd := int(r.inHandle) fd := int(r.inHandle)
b := make([]byte, 1) b := make([]byte, 1)
for { 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) _, err := util.Read(fd, b)
if err == nil { if err == nil {
r.ttyinChannel <- b[0] r.ttyinChannel <- b[0]
@@ -130,7 +132,7 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
select { select {
case bc := <-r.ttyinChannel: case bc := <-r.ttyinChannel:
return int(bc), true return int(bc), true
default: case <-time.After(timeoutInterval * time.Millisecond):
return 0, false return 0, false
} }
} else { } else {

View File

@@ -5,7 +5,6 @@ package tui
import ( import (
"os" "os"
"time" "time"
"unicode/utf8"
"runtime" "runtime"
@@ -13,6 +12,7 @@ import (
"github.com/gdamore/tcell/encoding" "github.com/gdamore/tcell/encoding"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
) )
func HasFullscreenRenderer() bool { func HasFullscreenRenderer() bool {
@@ -482,7 +482,6 @@ func (w *TcellWindow) Print(text string) {
} }
func (w *TcellWindow) printString(text string, pair ColorPair) { func (w *TcellWindow) printString(text string, pair ColorPair) {
t := text
lx := 0 lx := 0
a := pair.Attr() a := pair.Attr()
@@ -496,33 +495,28 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
Dim(a&Attr(tcell.AttrDim) != 0) Dim(a&Attr(tcell.AttrDim) != 0)
} }
for { gr := uniseg.NewGraphemes(text)
if len(t) == 0 { for gr.Next() {
break rs := gr.Runes()
}
r, size := utf8.DecodeRuneInString(t)
t = t[size:]
if r < rune(' ') { // ignore control characters if len(rs) == 1 {
continue r := rs[0]
} if r < rune(' ') { // ignore control characters
continue
if r == '\n' { } else if r == '\n' {
w.lastY++ w.lastY++
lx = 0 lx = 0
} else { continue
} else if r == '\u000D' { // skip carriage return
if r == '\u000D' { // skip carriage return
continue continue
} }
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
} }
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
}
lx += runewidth.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
} }
@@ -549,30 +543,32 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Underline(a&Attr(tcell.AttrUnderline) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0) Italic(a&Attr(tcell.AttrItalic) != 0)
for _, r := range text { gr := uniseg.NewGraphemes(text)
if r == '\n' { for gr.Next() {
rs := gr.Runes()
if len(rs) == 1 && rs[0] == '\n' {
w.lastY++ w.lastY++
w.lastX = 0 w.lastX = 0
lx = 0 lx = 0
} else { continue
var xPos = w.left + w.lastX + lx
// word wrap:
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
var yPos = w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
} }
// word wrap:
xPos := w.left + w.lastX + lx
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
yPos := w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
lx += runewidth.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
if w.lastX == w.width { if w.lastX == w.width {

View File

@@ -4,6 +4,7 @@ package tui
import ( import (
"io/ioutil" "io/ioutil"
"os"
"syscall" "syscall"
) )
@@ -29,3 +30,18 @@ func ttyname() string {
} }
return "" return ""
} }
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
func TtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
}
}
return os.Stdin
}
return in
}

View File

@@ -2,6 +2,13 @@
package tui package tui
import "os"
func ttyname() string { func ttyname() string {
return "" return ""
} }
// TtyIn on Windows returns os.Stdin
func TtyIn() *os.File {
return os.Stdin
}

View File

@@ -7,22 +7,29 @@ import (
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
) )
var _runeWidths = make(map[rune]int) // RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
// RuneWidth returns rune width width := 0
func RuneWidth(r rune, prefixWidth int, tabstop int) int { gr := uniseg.NewGraphemes(string(runes))
if r == '\t' { idx := 0
return tabstop - prefixWidth%tabstop for gr.Next() {
} else if w, found := _runeWidths[r]; found { rs := gr.Runes()
return w var w int
} else if r == '\n' || r == '\r' { if len(rs) == 1 && rs[0] == '\t' {
return 1 w = tabstop - (prefixWidth+width)%tabstop
} else {
w = runewidth.StringWidth(string(rs))
}
width += w
if limit > 0 && width > limit {
return width, idx
}
idx += len(rs)
} }
w := runewidth.RuneWidth(r) return width, -1
_runeWidths[r] = w
return w
} }
// Max returns the largest integer // Max returns the largest integer

View File

@@ -148,14 +148,15 @@ class Tmux
def prepare def prepare
tries = 0 tries = 0
begin begin
self.until do |lines| self.until(true) do |lines|
send_keys ' ', 'C-u', :Enter, 'hello', :Left, :Right message = "Prepare[#{tries}]"
lines[-1] == 'hello' send_keys ' ', 'C-u', :Enter, message, :Left, :Right
lines[-1] == message
end end
rescue Minitest::Assertion rescue Minitest::Assertion
(tries += 1) < 5 ? retry : raise (tries += 1) < 5 ? retry : raise
end end
send_keys 'C-u' send_keys 'C-u', 'C-l'
end end
private private
@@ -1863,6 +1864,195 @@ class TestGoFZF < TestBase
tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter
tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip } tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip }
end end
def test_toggle_preview_wrap
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
2.times do
tmux.until { |lines| assert_includes lines[2], '2nd line' }
tmux.send_keys 'C-w'
tmux.until do |lines|
assert_includes lines[2], 'wrapped'
assert_includes lines[3], '2nd line'
end
tmux.send_keys 'C-w'
end
end
def test_close
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.until { |lines| assert_includes lines[1], 'foo' }
tmux.send_keys 'C-c'
tmux.until { |lines| refute_includes lines[1], 'foo' }
tmux.send_keys '10'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
end
def test_select_deselect
tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 0, lines.select_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
end
def test_interrupt_execute
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'C-l'
tmux.until { |lines| assert lines.any_include?('executing 1') }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 99
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_kill_default_command_on_abort
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_default_command_on_accept
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_reload_command_on_abort
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_reload_command_on_accept
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_preview_header
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
tmux.until do |lines|
assert_includes lines[1], '4/1000'
assert_equal(%w[1 2 3 4 5], top5[lines])
end
tmux.send_keys '55'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(%w[1 2 3 55 56], top5[lines])
end
tmux.send_keys 'C-J'
tmux.until do |lines|
assert_equal(%w[1 2 3 58 59], top5[lines])
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 19, lines.match_count
assert_equal(%w[1 2 3 5 6], top5[lines])
end
tmux.send_keys 'C-K'
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
end
def test_unbind
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'ab'
tmux.until { |lines| assert_equal '> ab', lines[-1] }
tmux.send_keys 'c'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'dabcd'
tmux.until { |lines| assert_equal '> abcd', lines[-1] }
end
end end
module TestShell module TestShell
@@ -2033,7 +2223,7 @@ module CompletionTest
end end
# ~USERNAME**<TAB> # ~USERNAME**<TAB>
user = ENV['USER'] user = `whoami`.chomp
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{user}**", :Tab tmux.send_keys "cat ~#{user}**", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 } tmux.until { |lines| assert_operator lines.match_count, :>, 0 }