Compare commits

..

120 Commits

Author SHA1 Message Date
Junegunn Choi
30577b0c17 0.20.0 2019-12-18 01:07:25 +09:00
Junegunn Choi
212de25409 Fix incorrect header array mutation 2019-12-16 18:47:05 +09:00
Jan Edmund Lazo
5da8bbf45a [vim] Encode list source to codepage (#1794) 2019-12-16 14:41:03 +09:00
Jan Edmund Lazo
aa0e10ead7 [vim] Use cterm colors on Windows (#1793)
Truecolor does not work on default Windows terminal.
It is a problem in neovim GUIs.

https://github.com/sainnhe/edge/issues/5#issuecomment-565748240
2019-12-15 22:17:24 +09:00
msr1k
a9906c7c29 Add MSYS2 support as a vim plugin (#1677)
* Add MSYS2 support as a vim plugin

Add &shellcmdflag and TERM environment variable treatment.

- Make &shellcmdflag `/C` when &shell turns into `cmd.exe`
- Delete %TERM% environment variable before fzf execution

* Change shellescape default value depending on s:is_win flag

* Make TERM environment empty only when gui is running

* Stop checking &shell in fzf#shellescape function

This funcion's behavior is controlled by only if it is Windows or not.
So there is no need to check &shell.

* Take neovim into consideration when to set shellcmdflag

* Add &shellxquote control
2019-12-15 18:25:58 +09:00
Junegunn Choi
9fefe08b3f Revert README as preview-{fg,bg} are only available on master 2019-12-13 12:46:27 +09:00
Junegunn Choi
684bfff713 Update README/CHANGELOG 2019-12-13 12:43:34 +09:00
Junegunn Choi
3db6b88d82 Add preview-fg and preview-bg for --color
Close #1776
2019-12-12 23:03:17 +09:00
Junegunn Choi
8ae96774df Gutter color of 16-color theme should be undefined by default 2019-12-12 22:53:28 +09:00
Junegunn Choi
f68017d21e [windows/vim] Encode batchfile in current codepage
Backport https://github.com/junegunn/vim-plug/pull/913
2019-12-12 12:29:14 +09:00
Junegunn Choi
2b725a4db5 Defer resetting multi-selection on reload 2019-12-09 21:32:58 +09:00
Junegunn Choi
af1a5f130b Add clear-query and clear-selection
Close #1787
Related #1364
2019-12-07 14:44:24 +09:00
Junegunn Choi
86e3994e87 Properly clear list when --header-lines not filled on reload 2019-12-06 22:34:45 +09:00
Junegunn Choi
1e6ac5590e 'reload' action should be allowed even where there's no match
If the command template doesn't have any placeholder expressions.

    : | fzf --bind 'space:reload:seq 10'
2019-12-06 22:34:30 +09:00
Henré Botha
5e42b1c9f8 [ssh completion] Skip only aliases matching * (#1788)
This commit fixes a bug where lines that declare multiple hostnames get
omitted from completion entirely if one of the hostnames matches *. For
example:

	Host foo.com bar.dev baz.*
2019-12-06 17:58:53 +09:00
Junegunn Choi
9d842630c9 Mention _fzf_setup_completion helper function for bash 2019-12-06 12:03:34 +09:00
David Gray
77cb906dfe [completion] Add support for HostName lines in ~/.ssh/config (#1785)
Close #1783
2019-12-06 10:14:15 +09:00
Junegunn Choi
a59e846f74 Update installation instruction
Close #1707
Close #1779
2019-12-05 23:10:41 +09:00
infokiller
6e6340a0c9 Ignore zcompile output files (*.zwc files) (#1775) 2019-12-05 22:27:30 +09:00
John Purnell
357e82e51b [completion] Ignore hg repos (#1777)
* Update completion.bash
* Update completion.zsh
2019-12-05 22:27:04 +09:00
Junegunn Choi
394d8cfd18 Remove immediate flickering on reload action 2019-12-05 22:27:18 +09:00
Junegunn Choi
ef80bd401f Update installation instruction using Linux package managers
Added NixOS instruction. Close #1731
2019-12-01 23:48:48 +09:00
midchildan
f51d61d57a [zsh] Prevent the current directory from appearing as ~dir in prompts (#1774)
The zsh version of the cd widget sets the variable `dir` to the path of
the target directory before invoking `cd`. This causes zsh to treat the
target directory as a named directory, which has the effect of zsh
substituting '%~' with '~dir' instead of the proper path when it
performs prompt expansion.

This commit will cause the widget to unset `dir` before redrawing the
prompt to fix this issue.

Details of zsh prompt expansion can be found in:
http://zsh.sourceforge.net/Doc/Release/Prompt-Expansion.html
2019-12-01 23:20:26 +09:00
Junegunn Choi
1dd256a68a Update README-VIM 2019-11-29 18:32:49 +09:00
Junegunn Choi
85644aa3fb Revamp README-VIM.md 2019-11-23 01:54:52 +09:00
Junegunn Choi
effbc258bb Update CHANGELOG 2019-11-21 23:06:14 +09:00
Junegunn Choi
e615600ff1 Allow action composition over multiple --bind
# Note + prefix in the second bind expression
  fzf --bind u:up --bind u:+up
  fzf --bind u:up+up
2019-11-21 23:06:14 +09:00
Junegunn Choi
60465c4664 Fix parse error of --bind expression 2019-11-21 23:06:13 +09:00
Jan Edmund Lazo
c03c058bd5 [install] Support busybox uname on Windows (#1758) 2019-11-16 22:23:42 +09:00
Junegunn Choi
7238c8944d Update CHANGELOG 2019-11-16 01:23:01 +09:00
Junegunn Choi
9a41fd5327 0.19.0 2019-11-15 22:54:55 +09:00
Junegunn Choi
b471042037 Merge branch 'devel' 2019-11-15 22:53:08 +09:00
Junegunn Choi
2886f06977 Fix --preview-window noborder with non-default background color 2019-11-15 18:27:08 +09:00
Junegunn Choi
d630484eeb Update error message for --preview-window 2019-11-15 17:20:47 +09:00
Junegunn Choi
e24299239e Add --preview-window noborder option to disable preview border
Close #1699
2019-11-15 11:39:51 +09:00
Junegunn Choi
d2fa470165 Add --info=STYLE [default|inline|hidden]
Close #1738
2019-11-15 00:39:29 +09:00
Junegunn Choi
168453da71 More key chords for --bind
Close #1752
2019-11-14 22:39:35 +09:00
Junegunn Choi
23a06d63ac Update CHANGELOG and man pages 2019-11-13 01:27:39 +09:00
Junegunn Choi
751aa1944a Remove trailing whitespaces when using --with-nth 2019-11-12 23:20:09 +09:00
Junegunn Choi
05b5f3f845 'reload' action should reset multi-selection 2019-11-12 00:57:19 +09:00
Marco Hinz
16fc6862a8 [nvim] Handle SIGHUP in exit handler (#1749)
In recent Nvim versions, an "Error running ..." message is shown even for normal
use cases, such as:

    :Files
    <c-\><c-n>
    :close

Closing the window will :bwipeout! the terminal buffer, because fzf sets
bufhiden=wipe.

When deleting the terminal buffer while fzf is still running, Nvim sends SIGHUP.
This happens for quite some time already, but the bug only manifests since this
commit:

  https://github.com/neovim/neovim/commit/939d9053b

It's The Right Thing to do when the application exited due to a signal.

Before that commit, no "Error running ..." message was shown, because 1 (instead
of 128 + 1 == SIGHUP) was returned which the exit handler in fzf.vim treats as
"NO MATCH".
2019-11-12 00:47:05 +09:00
Junegunn Choi
7e1c0f39e7 'reload' action should reset --header-lines 2019-11-12 00:10:24 +09:00
Junegunn Choi
deccf20a35 Fix regression of select-all 2019-11-11 23:31:31 +09:00
Junegunn Choi
73c0a645e0 Remove unnecessary reader barrier on --filter mode 2019-11-11 12:53:03 +09:00
Junegunn Choi
e975bd0c8d Add test cases for --phony and reload action 2019-11-10 13:13:45 +09:00
Junegunn Choi
78da928727 Experimental implementation of "reload" action
# Reload input list with different sources
  seq 10 | fzf --bind 'ctrl-a:reload(seq 100),ctrl-b:reload(seq 1000)'

  # Reload as you type
  seq 10 | fzf --bind 'change:reload:seq {q}' --phony

  # Integration with ripgrep
  RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
  INITIAL_QUERY=""
  FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
    fzf --bind "change:reload:$RG_PREFIX {q} || true" \
        --ansi --phony --query "$INITIAL_QUERY"

Close #751
Close #965
Close #974
Close #1736
Related #1723
2019-11-10 11:43:37 +09:00
Junegunn Choi
11962dabba Add --phony option for disabling search
With --phony, fzf becomes a simply selector interface without its own
search functionality. The query string is only used for building the
command for preview or execute action.

Close #1723
2019-11-10 11:36:55 +09:00
Junegunn Choi
dceb5d09cd RTFM, please 2019-11-08 14:09:49 +09:00
Alexandr
b4cccf23d4 Improvements to code quality and readability (#1737)
* Remove 1 unused field and 3 unused functions

unused elements fount by running
golangci-lint run --disable-all --enable unused

src/result.go:19:2: field `index` is unused (unused)
        index  int32
        ^
src/tui/light.go:716:23: func `(*LightWindow).stderr` is unused (unused)
func (w *LightWindow) stderr(str string) {
                      ^
src/terminal.go:1015:6: func `numLinesMax` is unused (unused)
func numLinesMax(str string, max int) int {
     ^
src/tui/tui.go:167:20: func `ColorPair.is24` is unused (unused)
func (p ColorPair) is24() bool {
                   ^

* Address warnings from "gosimple" linter

src/options.go:389:83: S1003: should use strings.Contains(str, ",,,") instead (gosimple)
        if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
                                                                                         ^
src/options.go:630:18: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        executeRegexp = regexp.MustCompile(
                        ^
src/terminal.go:29:16: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})")
                      ^
src/terminal_test.go:92:10: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple)
        regex = regexp.MustCompile("\\w+")
                ^

* Address warnings from "staticcheck" linter

src/algo/algo.go:374:2: SA4006: this value of `offset32` is never used (staticcheck)
        offset32, T := alloc32(offset32, slab, N)
        ^
src/algo/algo.go:456:2: SA4006: this value of `offset16` is never used (staticcheck)
        offset16, C := alloc16(offset16, slab, width*M)
        ^
src/tui/tui.go:119:2: SA9004: only the first constant in this group has an explicit type (staticcheck)
        colUndefined Color = -2
        ^
2019-11-05 09:46:51 +09:00
zhaoyunfeng
b911af200c [zsh-completion] Fix prefix extraction when triggers start with ';' 2019-11-03 00:33:30 +09:00
Junegunn Choi
68683c444f Fix argument parser for -m
/cc @tessus
2019-11-02 20:44:21 +09:00
Junegunn Choi
a185593d65 Remove unnecessary map lookup 2019-11-02 19:55:05 +09:00
Junegunn Choi
525040238e Fix behavior of 'deselect-all' to only deselect matches
To make it consistent with select-all and toggle-all.

Close #1364
2019-11-02 19:41:59 +09:00
Junegunn Choi
33f89a08f3 Build with Go 1.13 2019-11-02 14:59:22 +09:00
Junegunn Choi
11645e1fac Fix flaky test case 2019-11-02 14:55:13 +09:00
Junegunn Choi
6390140539 [vim/windows] Use chcp only if sed is in PATH
https://github.com/junegunn/vim-plug/pull/891
2019-11-02 14:35:21 +09:00
Junegunn Choi
072066c49c --multi to take optional argument to limit the number of selection
Close #1718
Related #688
2019-11-02 14:25:12 +09:00
Junegunn Choi
a2e9366c84 Fix flaky test case 2019-11-02 13:15:01 +09:00
Simon Fraser
391669a451 Add 'f' flag for placeholder expression (#1733)
If present the contents of the selection will be placed in a temporary file,
and the filename will be placed into the string instead.
2019-10-27 23:50:12 +09:00
Junegunn Choi
0c6c76e081 [zsh] Suppress global alias expansion in widget functions
Close #1708
2019-10-17 14:51:57 +09:00
stiletto
f1520bdde6 Support building on machines with uname -m == "aarch64" (#1710) 2019-10-11 22:11:06 +09:00
Junegunn Choi
3089880f18 [vim/windows] Fix chcp parsing for the current codepage
https://github.com/junegunn/vim-plug/pull/888
2019-10-08 09:41:22 +09:00
Junegunn Choi
ab11b74be4 [vim] Output of chcp was not parsed correctly
By @gh4w and @janlazo

See 68b31a4a66
2019-09-29 14:53:45 +09:00
Junegunn Choi
a5a97be017 [bash-completion] Properly handle exit event
Related #1704
2019-09-29 14:45:58 +09:00
Junegunn Choi
80b5bc1b68 [vim] Shell-escape --color option generated by fzf#wrap
Fix https://github.com/junegunn/fzf.vim/issues/855
2019-09-09 12:12:42 +09:00
Junegunn Choi
5c7dcaffe8 [bash-completion] _fzf_setup_completion to retain previous options 2019-08-09 11:11:29 +09:00
Junegunn Choi
5095899245 [bash-completion] Add _fzf_setup_completion to enable fuzzy completion
While we can attach `_fzf_path_completion` or `_fzf_dir_completion` to
any command using the standard bash complete command, the functionality
of the existing completion function is lost.

Use _fzf_setup_completion if you want to extend the existing function
with fuzzy completion instead of completely replacing it.

e.g. _fzf_setup_completion path kubectl
2019-08-08 15:35:52 +09:00
Ross Smith II
4800e5d2ae Add scoop mention (#1646) 2019-08-06 14:11:28 +09:00
Junegunn Choi
3b1e37f718 Fix #1657: alt-0 to alt-9 2019-08-06 14:06:58 +09:00
Christian Muehlhaeuser
6577388250 os.Kill signal cannot be trapped (#1641) 2019-07-19 13:24:46 +09:00
Christian Muehlhaeuser
3b9dbd4146 Code cleanup: remove unnecessary string conversions (#1642) 2019-07-19 13:23:18 +09:00
Christian Muehlhaeuser
a1260feeed Code cleanup (#1640)
- Replaced time.Now().Sub() with time.Since()
- Replaced unnecessary string/byte slice conversions
- Removed obsolete return and value assignment in range loop
2019-07-19 13:22:35 +09:00
Dan Čermák
7322504ad0 Add installation instructions for openSUSE (#1631) 2019-07-17 11:19:13 +09:00
miclill
de569f0052 Add Debian install instructions (#1620) 2019-07-17 11:18:07 +09:00
ssjhv
e7097a9d25 [fish] Remove perl from fish key bindings (#1635)
Perl was used to remove the trailing newline character, but fzf already
has --print0 to use null character as terminators, and fish read -z is
expecting null character as terminators. There is no reason to depend on
perl if --print0 is passed to fzf invocation.
2019-07-13 14:47:51 +09:00
charlton1
c1dbc800e5 [vim] Fix name-based colors for GVim/8.0 w/o builtin terminal (#1634)
(i.e. spawn xterm)
2019-07-09 11:08:36 +09:00
Junegunn Choi
951746297e Fix invalid layout example 2019-06-08 23:29:04 +09:00
Junegunn Choi
984304568d Remove outdated GVim instruction
The section is no longer relevant since (G)Vim 8 or above has builtin
terminal emulator.
2019-06-08 23:22:05 +09:00
Junegunn Choi
723217bdea Add fzf#run tutorial to README-VIM.md 2019-06-08 23:17:30 +09:00
Mateusz Piotrowski
0fdb71f7e4 Add FreeBSD installation instructions (#1569) 2019-06-05 09:26:04 +09:00
Junegunn Choi
12ce76b56a [bash] Make sure to execute builtin history
Fix #1592
2019-06-03 18:46:01 +09:00
Michael Kelley
0030d18448 Update sys module to newer version (#1582)
A newer version of the sys module is needed for pull request #1341
2019-05-26 11:06:46 +09:00
Junegunn Choi
0e3e6ac442 Disallow preview scroll when the content just fits the window 2019-05-21 16:35:45 +09:00
Dominik Reller
430e8193e0 [install] Remove unused variable in install script (#1571) 2019-05-07 11:34:55 +09:00
Jesus Briales
03e8ed4d88 [bash-completion] Fix custom completion with dynamic loader enabled for non-standard command names (#1564)
Related to #1170.

Fix the solution for commands with non-standard names
where `$cmd` and `$orig_cmd` differ. e.g. `s.foo` -> `s_foo`
2019-05-01 02:35:51 +09:00
Junegunn Choi
ef492f6178 Output --help message to standard output
Close #1554
2019-04-21 18:02:34 +09:00
Alexey Samoshkin
8eea45ef50 Add demo screencast video to README (#1557) 2019-04-18 18:49:30 +09:00
Junegunn Choi
ff951341c9 0.18.0 2019-03-31 11:22:38 +09:00
Junegunn Choi
df570afd52 [docker] Fix gem install option in Dockerfile 2019-03-31 11:22:12 +09:00
Junegunn Choi
07d755df11 Fix regression of prompt display 2019-03-30 02:07:09 +09:00
Junegunn Choi
37585bd5a5 Disable preview scroll if the content fits on the screen
Close #1540
2019-03-29 18:28:45 +09:00
Junegunn Choi
89e24bf8f2 Fix ineffective break statement 2019-03-29 17:29:24 +09:00
Junegunn Choi
8d2fcd3518 Avoid unnecessary redraw of the preview window 2019-03-29 15:12:46 +09:00
Junegunn Choi
f39ab3875e Redraw prompt only when necessary 2019-03-29 15:02:31 +09:00
AnakinXL
82efe6c60d [doc] Add bat for preview syntax highlighting example (#1538)
Similar to this PR from fzf.vim:
https://github.com/junegunn/fzf.vim/pull/712
2019-03-29 10:31:17 +09:00
Junegunn Choi
75972d59a8 Add --no-unicode option to draw borders in ASCII characters
Close ##1533
2019-03-29 02:11:03 +09:00
Junegunn Choi
e7d60aac9c [vim] Do not restore cwd when autochdir is set and buffer changed
Close #1539
2019-03-28 13:57:04 +09:00
Junegunn Choi
a0bfbdd49c [vim] Increase window height by 2 when --border is set
Close #1535
2019-03-26 16:42:35 +09:00
Junegunn Choi
ba594982f0 Add MacPorts instruction
Close #1521
2019-03-18 18:45:57 +09:00
Junegunn Choi
2157f4f193 Add color option for gutter
fzf --color gutter:-1

Close #1529
Close #1468
2019-03-17 15:52:38 +09:00
Junegunn Choi
309bae423c [zsh-completion] Suppress "no matches found" message 2019-03-14 01:10:51 +09:00
Junegunn Choi
4f8bf2ae78 [install] Avoid generating empty component in $PATH
Fix #1517
2019-03-08 12:38:36 +09:00
Junegunn Choi
85c1f8a9e0 Always prepend ANSI reset code before re-assembling tokens 2019-03-07 15:29:57 +09:00
Junegunn Choi
e00e7e1e56 Remove unnecessary ANSI code injection 2019-03-07 02:02:02 +09:00
Junegunn Choi
1a6defdbcc Use simple string concatenation instead of using fmt.Sprintf 2019-03-07 02:00:31 +09:00
Junegunn Choi
ef577a6509 Preserve the original color of each token when using --with-nth with --ansi
Close #1500
2019-03-06 19:05:05 +09:00
Junegunn Choi
b7c6838e45 [install] Fix symlink log
Related #1466
2019-03-05 14:45:29 +09:00
Junegunn Choi
91d04cec5c [install] Print better error message when fzf --version failed
Related #1466
2019-03-05 14:44:29 +09:00
Rui Coelho
3bd8441079 [completion] Look up on ~/.ssh/config.d/* files when doing ssh host complete (#1420) 2019-02-28 16:40:41 +09:00
Junegunn Choi
8cf45a5197 [shell] Skip loading completion code on non-interactive shell
This change is not required if you use the install script to generate
~/.fzf.bash or ~/.fzf.zsh which already has the proper guard statement.

Close #1474
2019-02-28 16:13:59 +09:00
Junegunn Choi
8dc1377efb Export FZF_PREVIEW_LINES and FZF_PREVIEW_COLUMNS to preview process
fzf will still override LINES and COLUMNS as before but they may not
hold the correct values depending on the default shell.

Close #1314
2019-02-22 14:36:30 +09:00
Junegunn Choi
6c32148f90 Add placeholder expression for zero-based item index: {n} and {+n}
Close #1482
2019-02-19 01:12:57 +09:00
Junegunn Choi
315e568de0 Update build instruction
Close #1485
2019-01-31 18:43:10 +09:00
Junegunn Choi
5d16b28869 Fix tab width after ANSI reset code in preview window
Close #1423
2018-12-22 11:52:18 +09:00
Junegunn Choi
5624a89231 Inverse-only matches should not reorder the remaining results
Fix #1458
2018-12-19 23:05:29 +09:00
Junegunn Choi
63c42b14f2 Remove trailing spaces in Makefile 2018-12-13 15:17:30 +09:00
Stefan Tatschner
6f1eaa9b39 Use go modules and simplify build (#1444)
* Update .travis.yml and use stages

This updates the .travis.yml configuration to use separate stages for
unittests and CLI tests. The output is now clearer, since for unittests
and CLI tests separate web pages are available.

* Use go modules and simplify build
2018-12-13 14:36:15 +09:00
Junegunn Choi
ca42e5e00a Avoid unnecessary redraw of preview window
Close #1455
2018-12-13 10:58:57 +09:00
Junegunn Choi
61feee690c Render preview window when the initial query fails to match
Only if preview template contains {q}

Fix #1452
Related #1307
2018-12-05 18:45:55 +09:00
Christian Muehlhaeuser
d4ed955aee Typo & grammar fixes in README (#1413) 2018-11-09 16:50:16 +09:00
49 changed files with 2128 additions and 871 deletions

View File

@@ -1,30 +1,22 @@
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
<!-- Check all that apply [x] -->
- Category
- [ ] fzf binary
- [ ] fzf-tmux script
- [ ] Key bindings
- [ ] Completion
- [ ] Vim
- [ ] Neovim
- [ ] Etc.
- [ ] I have read through the manual page (`man fzf`)
- [ ] I have the latest version of fzf
- [ ] I have searched through the existing issues
## Info
- OS
- [ ] Linux
- [ ] Mac OS X
- [ ] Windows
- [ ] Windows Subsystem for Linux
- [ ] Etc.
- Shell
- [ ] bash
- [ ] zsh
- [ ] fish
<!--
### Before submitting
- Make sure that you have the latest version of fzf
- If you use tmux, make sure $TERM starts with "screen"
- For more Vim stuff, check out https://github.com/junegunn/fzf.vim
Describe your problem or suggestion from here ...
-->
## Problem / Steps to reproduce

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ Gemfile.lock
doc/tags
vendor
gopath
*.zwc

View File

@@ -1,20 +1,27 @@
language: ruby
dist: trusty
sudo: required
matrix:
language: go
dist: xenial
addons:
apt:
sources:
- sourceline: "ppa:pi-rho/dev"
- sourceline: "ppa:fish-shell/release-2"
packages:
- tmux
- zsh
- fish
env:
- GO111MODULE=on
jobs:
include:
- env: TAGS=
rvm: 2.3.3
# - env: TAGS=tcell
# rvm: 2.3.3
- stage: unittest
go: "1.13.x"
script: make && make test
install:
- sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update
- sudo apt-get install -y tmux zsh fish
- stage: cli
go: "1.13.x"
rvm: "2.5"
script: |
make install && ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
script: |
make test install &&
./install --all &&
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]

View File

@@ -6,12 +6,10 @@ Build instructions
### Prerequisites
- `go` executable in $PATH
- Go 1.11 or above
### Using Makefile
Makefile will set up and use its own `$GOPATH` under the project root.
```sh
# Build fzf binary for your platform in target
make

View File

@@ -1,6 +1,109 @@
CHANGELOG
=========
0.20.0
------
- Customizable preview window color (`preview-fg` and `preview-bg` for `--color`)
```sh
fzf --preview 'cat {}' \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' \
--border --height 20 --layout reverse --info inline
```
- Removed the immediate flicking of the screen on `reload` action.
```sh
: | fzf --bind 'change:reload:seq {q}' --phony
```
- Added `clear-query` and `clear-selection` actions for `--bind`
- It is now possible to split a composite bind action over multiple `--bind`
expressions by prefixing the later ones with `+`.
```sh
fzf --bind 'ctrl-a:up+up'
# Can be now written as
fzf --bind 'ctrl-a:up' --bind 'ctrl-a:+up'
# This is useful when you need to write special execute/reload form (i.e. `execute:...`)
# to avoid parse errors and add more actions to the same key
fzf --multi --bind 'ctrl-l:select-all+execute:less {+f}' --bind 'ctrl-l:+deselect-all'
```
- Fixed parse error of `--bind` expression where concatenated execute/reload
action contains `+` character.
```sh
fzf --multi --bind 'ctrl-l:select-all+execute(less {+f})+deselect-all'
```
- Fixed bugs of reload action
- Not triggered when there's no match even when the command doesn't have
any placeholder expressions
- Screen not properly cleared when `--header-lines` not filled on reload
0.19.0
------
- Added `--phony` option which completely disables search functionality.
Useful when you want to use fzf only as a selector interface. See below.
- Added "reload" action for dynamically updating the input list without
restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn
more about it.
```sh
# Using fzf as the selector interface for ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foo"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY' || true" \
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
--ansi --phony --query "$INITIAL_QUERY"
```
- `--multi` now takes an optional integer argument which indicates the maximum
number of items that can be selected
```sh
seq 100 | fzf --multi 3 --reverse --height 50%
```
- If a placeholder expression for `--preview` and `execute` action (and the
new `reload` action) contains `f` flag, it is replaced to the
path of a temporary file that holds the evaluated list. This is useful
when you multi-select a large number of items and the length of the
evaluated string may exceed [`ARG_MAX`][argmax].
```sh
# Press CTRL-A to select 100K items and see the sum of all the numbers
seq 100000 | fzf --multi --bind ctrl-a:select-all \
--preview "awk '{sum+=\$1} END {print sum}' {+f}"
```
- `deselect-all` no longer deselects unmatched items. It is now consistent
with `select-all` and `toggle-all` in that it only affects matched items.
- Due to the limitation of bash, fuzzy completion is enabled by default for
a fixed set of commands. A helper function for easily setting up fuzzy
completion for any command is now provided.
```sh
# usage: _fzf_setup_completion path|dir COMMANDS...
_fzf_setup_completion path git kubectl
```
- Info line style can be changed by `--info=STYLE`
- `--info=default`
- `--info=inline` (same as old `--inline-info`)
- `--info=hidden`
- Preview window border can be disabled by adding `noborder` to
`--preview-window`.
- When you transform the input with `--with-nth`, the trailing white spaces
are removed.
- `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind`
- See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details
[argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument
0.18.0
------
- Added placeholder expression for zero-based item index: `{n}` and `{+n}`
- `fzf --preview 'echo {n}: {}'`
- Added color option for the gutter: `--color gutter:-1`
- Added `--no-unicode` option for drawing borders in non-Unicode, ASCII
characters
- `FZF_PREVIEW_LINES` and `FZF_PREVIEW_COLUMNS` are exported to preview process
- fzf still overrides `LINES` and `COLUMNS` as before, but they may be
reset by the default shell.
- Bug fixes and improvements
- See https://github.com/junegunn/fzf/milestone/14?closed=1
- Built with Go 1.12.1
0.17.5
------

View File

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

View File

@@ -1,17 +1,9 @@
ifndef GOOS
GOOS := $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
endif
GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE))
GOPATH := $(ROOT_DIR)/gopath
SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src
VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor
export GOPATH
GLIDE_YAML := glide.yaml
GLIDE_LOCK := glide.lock
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) $(MAKEFILE)
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)"
@@ -50,6 +42,8 @@ else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),armv8l)
BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),aarch64)
BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),ppc64le)
BINARY := $(BINARYPPC64LE)
else
@@ -90,19 +84,8 @@ release-all: clean test
GOOS=openbsd make release
GOOS=windows make release
$(SRC_LINK):
mkdir -p $(shell dirname $(SRC_LINK))
ln -sf $(ROOT_DIR)/src $(SRC_LINK)
$(VENDOR_LINK):
mkdir -p $(shell dirname $(VENDOR_LINK))
ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK)
vendor: $(GLIDE_YAML)
go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@
test: $(SOURCES) vendor
SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" \
test: $(SOURCES)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \
@@ -111,29 +94,29 @@ test: $(SOURCES) vendor
install: bin/fzf
clean:
rm -rf target
$(RM) -r target
target/$(BINARY32): $(SOURCES) vendor
GOARCH=386 go build $(BUILD_FLAGS) -o $@
target/$(BINARY32): $(SOURCES)
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES) vendor
GOARCH=amd64 go build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) vendor
GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM6): $(SOURCES) vendor
GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM6): $(SOURCES)
GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM7): $(SOURCES) vendor
GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM7): $(SOURCES)
GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYARM8): $(SOURCES) vendor
GOARCH=arm64 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM8): $(SOURCES)
GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYPPC64LE): $(SOURCES) vendor
GOARCH=ppc64le go build $(BUILD_FLAGS) -o $@
target/$(BINARYPPC64LE): $(SOURCES)
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
@@ -146,4 +129,8 @@ docker-test:
docker build -t fzf-arch .
docker run -it fzf-arch
.PHONY: all release release-all test install clean docker docker-test
update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all release release-all test install clean docker docker-test update

View File

@@ -1,16 +1,33 @@
FZF Vim integration
===================
This repository only enables basic integration with Vim. If you're looking for
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
Summary
-------
(Note: To use fzf in GVim, an external terminal emulator is required.)
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them.
1. **`fzf#run([spec dict])`**
- Starts fzf inside Vim with the given spec
- `:call fzf#run({'source': 'ls'})`
2. **`fzf#wrap([spec dict]) -> (dict)`**
- Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`)
- `:echo fzf#wrap({'source': 'ls'})`
- We usually *wrap* a spec with `fzf#wrap` before passing it to `fzf#run`
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
3. **`:FZF [fzf_options string] [path string]`**
- Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript
to implement custom commands
- If you're looking for more such commands, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
The most important of all is `fzf#run`, but it would be easier to understand
the whole if we start off with `:FZF` command.
`:FZF[!]`
---------
If you have set up fzf for Vim, `:FZF` command will be added.
```vim
" Look for files under current directory
:FZF
@@ -18,8 +35,8 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" With fzf command-line options
:FZF --reverse --info=inline /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
@@ -42,9 +59,6 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
#### Examples
@@ -75,9 +89,10 @@ let g:fzf_layout = { 'down': '~40%' }
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
let g:fzf_layout = { 'window': '10new' }
" Customize fzf colors to match your color scheme
" - fzf#wrap translates this to a set of `--color` options
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
@@ -93,36 +108,85 @@ let g:fzf_colors =
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
" Enable per-command history
" - History files will be stored in the specified directory
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
" 'previous-history' instead of 'down' and 'up'.
let g:fzf_history_dir = '~/.local/share/fzf-history'
```
`fzf#run`
---------
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
`fzf#run()` function is the core of Vim integration. It takes a single
dictionary argument, *a spec*, and starts fzf process accordingly. At the very
least, specify `sink` option to tell what it should do with the selected
entry.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
```vim
call fzf#run({'sink': 'e'})
```
We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
directory. When you select one, it will open it with the sink, `:e` command.
If you want to open it in a new tab, you can pass `:tabedit` command instead
as the sink.
```vim
call fzf#run({'sink': 'tabedit'})
```
Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's
equivalent to running `git ls-files | fzf` on shell.
```vim
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
```
fzf options can be specified as `options` entry in spec dictionary.
```vim
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
```
You can also pass a layout option if you don't want fzf window to take up the
entire screen.
```vim
" up / down / left / right / window are allowed
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
```
`source` doesn't have to be an external shell command, you can pass a Vim
array as the source. In the next example, we pass the names of color
schemes as the source to implement a color scheme selector.
```vim
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
\ 'fnamemodify(v:val, ":t:r")'),
\ 'sink': 'colo', 'left': '25%'})
```
The following table summarizes the available options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
`options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping
issues on different platforms.
should suffice, but prefer to use list type to avoid escaping issues.
```vim
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
@@ -132,65 +196,131 @@ call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
`fzf#wrap`
----------
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
We have seen that several aspects of `:FZF` command can be configured with
a set of global option variables; different ways to open files
(`g:fzf_action`), window position and size (`g:fzf_layout`), color palette
(`g:fzf_colors`), etc.
So how can we make our custom `fzf#run` calls also respect those variables?
Simply by *"wrapping"* the spec dictionary with `fzf#wrap` before passing it
to `fzf#run`.
- **`fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`**
- All arguments are optional. Usually we only need to pass a spec dictionary.
- `name` is for managing history files. It is ignored if
`g:fzf_history_dir` is not defined.
- `fullscreen` can be either `0` or `1` (default: 0).
`fzf#wrap` takes a spec and returns an extended version of it (also
a dictionary) with additional options for addressing global preferences. You
can examine the return value of it like so:
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
echo fzf#wrap({'source': 'ls'})
```
fzf inside terminal buffer
--------------------------
After we *"wrap"* our spec, we pass it to `fzf#run`.
```vim
call fzf#run(fzf#wrap({'source': 'ls'}))
```
Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings and it opens fzf
window according to `g:fzf_layout` setting.
To make it easier to use, let's define `LS` command.
```vim
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
```
Type `:LS` and see how it works.
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
`:FZF!`. Add `-bang` to command definition, and use `<bang>` value to set
the last `fullscreen` argument of `fzf#wrap` (see `:help <bang>`).
```vim
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
```
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.
```vim
command! -bang -complete=dir -nargs=* LS
\ 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 unique name to our command and pass it as the first argument to `fzf#wrap`.
```vim
" 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.
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
```
Tips
----
### fzf inside terminal buffer
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with the non-default layout
- On Terminal Vim with a non-default layout
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
### Hide statusline
When fzf starts in a terminal buffer, you may want to hide the statusline of
the containing buffer.
#### Starting fzf in Neovim floating window
```vim
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
" Using floating windows of Neovim to start fzf
if has('nvim')
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
function! FloatingFZF()
let width = float2nr(&columns * 0.9)
let height = float2nr(&lines * 0.6)
let opts = { 'relative': 'editor',
\ 'row': (&lines - height) / 2,
\ 'col': (&columns - width) / 2,
\ 'width': width,
\ 'height': height }
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
endfunction
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
endif
```
GVim
----
#### Hide statusline
With the latest version of GVim, fzf will start inside the builtin terminal
emulator of Vim. Please note that this terminal feature of Vim is still young
and unstable and you may run into some issues.
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
the window.
If you have an older version of GVim, you need an external terminal emulator
to start fzf with. `xterm` command is used by default, but you can customize
it with `g:fzf_launcher`.
For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you
might want to temporarily disable the statusline for a cleaner look.
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
if has('nvim') && !exists('g:fzf_layout')
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up.
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
[License](LICENSE)
------------------
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Copyright (c) 2019 Junegunn Choi

199
README.md
View File

@@ -25,10 +25,9 @@ Table of Contents
* [Installation](#installation)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
* [Using git](#using-git)
* [As Vim plugin](#as-vim-plugin)
* [Arch Linux](#arch-linux)
* [Fedora](#fedora)
* [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows)
* [As Vim plugin](#as-vim-plugin)
* [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf)
* [Usage](#usage)
@@ -37,6 +36,7 @@ Table of Contents
* [Search syntax](#search-syntax)
* [Environment variables](#environment-variables)
* [Options](#options)
* [Demo](#demo)
* [Examples](#examples)
* [fzf-tmux script](#fzf-tmux-script)
* [Key bindings for command line](#key-bindings-for-command-line)
@@ -54,7 +54,6 @@ Table of Contents
* [Preview window](#preview-window)
* [Tips](#tips)
* [Respecting .gitignore](#respecting-gitignore)
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
* [Fish shell](#fish-shell)
* [Related projects](#related-projects)
* [<a href="LICENSE">License</a>](#license)
@@ -88,6 +87,10 @@ brew install fzf
$(brew --prefix)/opt/fzf/install
```
fzf is also available [via MacPorts][portfile]: `sudo port install fzf`
[portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile
### Using git
Alternatively, you can "git clone" this repository to any directory and run
@@ -98,10 +101,45 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
### Using Linux package managers
| Distro | Command |
| --- | --- |
| Alpine Linux | `sudo apk add fzf` |
| Arch Linux | `sudo pacman -S fzf` |
| Debian | `sudo apt-get install fzf` |
| Fedora | `sudo dnf install fzf` |
| FreeBSD | `pkg install fzf` |
| NixOS | `nix-env -iA nixpkgs.fzf` |
| openSUSE | `sudo zypper install fzf` |
Shell extensions (key bindings and fuzzy auto-completion) and Vim/Neovim
plugin may or may not be enabled by default depending on the package manager.
Refer to the package documentation for more information.
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available via [Chocolatey][choco] and [Scoop][scoop]:
| Package manager | Command |
| --- | --- |
| Chocolatey | `choco install fzf` |
| Scoop | `scoop install fzf` |
[choco]: https://chocolatey.org/packages/fzf
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
Known issues and limitations on Windows can be found on [the wiki
page][windows-wiki].
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
### As Vim plugin
Once you have fzf installed, you can enable it inside Vim simply by adding the
directory to `&runtimepath` in your Vim configuration file as follows:
directory to `&runtimepath` in your Vim configuration file. The path may
differ depending on the package manager.
```vim
" If installed using Homebrew
@@ -127,50 +165,12 @@ But instead of separately installing fzf on your system (using Homebrew or
vim-plug to do both.
```vim
" PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run install script
" PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run the install script
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
" Both options are optional. You don't have to install fzf in ~/.fzf
" and you don't have to run install script if you use fzf only in Vim.
" and you don't have to run the install script if you use fzf only in Vim.
```
### Arch Linux
```sh
sudo pacman -S fzf
```
### Fedora
fzf is available in Fedora 26 and above, and can be installed using the usual
method:
```sh
sudo dnf install fzf
```
Shell completion and plugins for vim or neovim are enabled by default. Shell
key bindings are installed but not enabled by default. See Fedora's package
documentation (/usr/share/doc/fzf/README.Fedora) for more information.
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available as a [Chocolatey package][choco].
[choco]: https://chocolatey.org/packages/fzf
```sh
choco install fzf
```
However, other components of the project may not work on Windows. Known issues
and limitations can be found on [the wiki page][windows-wiki]. You might want
to consider installing fzf on [Windows Subsystem for Linux][wsl] where
everything runs flawlessly.
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Upgrading fzf
-------------
@@ -279,6 +279,13 @@ or `py`.
See the man page (`man fzf`) for the full list of options.
#### Demo
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
<img src="https://i.imgur.com/vtG8olE.png" width="640">
</a>
Examples
--------
@@ -312,16 +319,16 @@ fullscreen mode.
fzf --height 40%
```
Key bindings for command line
Key bindings for command-line
-----------------------------
The install script will setup the following key bindings for bash, zsh, and
fish.
- `CTRL-T` - Paste the selected files and directories onto the command line
- `CTRL-T` - Paste the selected files and directories onto the command-line
- Set `FZF_CTRL_T_COMMAND` to override the default command
- Set `FZF_CTRL_T_OPTS` to pass additional options
- `CTRL-R` - Paste the selected command from history onto the command line
- `CTRL-R` - Paste the selected command from history onto the command-line
- If you want to see the commands in chronological order, press `CTRL-R`
again which toggles sorting by relevance
- Set `FZF_CTRL_R_OPTS` to pass additional options
@@ -373,7 +380,7 @@ cd ~/github/fzf**<TAB>
#### 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.
```sh
@@ -426,11 +433,12 @@ _fzf_compgen_dir() {
On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other
commands as well like follows.
commands as well by using `_fzf_setup_completion` helper function.
```sh
complete -F _fzf_path_completion -o default -o bashdefault ag
complete -F _fzf_dir_completion -o default -o bashdefault tree
# usage: _fzf_setup_completion path|dir COMMANDS...
_fzf_setup_completion path ag git kubectl
_fzf_setup_completion dir tree
```
Vim plugin
@@ -443,7 +451,7 @@ Advanced topics
### Performance
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
options that affect the performance.
@@ -454,7 +462,7 @@ options that affect the performance.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line.
- If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use faster greedy
`--algo=v1` (the default being `v2`) to make fzf use a faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended.
@@ -475,7 +483,7 @@ See *KEY BINDINGS* section of the man page for details.
### Preview window
When `--preview` option is set, fzf automatically starts external process with
When `--preview` option is set, fzf automatically starts an external process with
the current line as the argument and shows the result in the split window.
```bash
@@ -483,7 +491,7 @@ the current line as the argument and shows the result in the split window.
fzf --preview 'cat {}'
```
Since preview window is updated only after the process is complete, it's
Since the preview window is updated only after the process is complete, it's
important that the command finishes quickly.
```bash
@@ -491,35 +499,50 @@ important that the command finishes quickly.
fzf --preview 'head -100 {}'
```
Preview window supports ANSI colors, so you can use programs that
Preview window supports ANSI colors, so you can use any program that
syntax-highlights the content of a file.
- Bat: https://github.com/sharkdp/bat
- Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php
- CodeRay: http://coderay.rubychan.de/
- Rouge: https://github.com/jneen/rouge
```bash
# Try highlight, coderay, rougify in turn, then fall back to cat
fzf --preview '[[ $(file --mime {}) =~ binary ]] &&
echo {} is a binary file ||
(highlight -O ansi -l {} ||
coderay {} ||
rougify {} ||
cat {}) 2> /dev/null | head -500'
fzf --preview 'bat --style=numbers --color=always {} | head -500'
```
You can customize the size and position of the preview window using
`--preview-window` option. For example,
You can customize the size, position, and border of the preview window using
`--preview-window` option, and the foreground and background color of it with
`--color` option. For example,
```bash
fzf --height 40% --reverse --preview 'file {}' --preview-window down:1
fzf --height 40% --layout reverse --info inline --border \
--preview 'file {}' --preview-window down:1:noborder \
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
```
See the man page (`man fzf`) for the full list of options.
For more advanced examples, see [Key bindings for git with fzf][fzf-git]
([code](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236)).
[fzf-git]: https://junegunn.kr/2016/07/fzf-git/
----
Since fzf is a general-purpose text filter rather than a file finder, **it is
not a good idea to add `--preview` option to your `$FZF_DEFAULT_OPTS`**.
```sh
# *********************
# ** DO NOT DO THIS! **
# *********************
export FZF_DEFAULT_OPTS='--preview "bat --style=numbers --color=always {} | head -500"'
# bat doesn't work with any input other than the list of files
ps -ef | fzf
seq 100 | fzf
history | fzf
```
Tips
----
@@ -552,45 +575,17 @@ hidden files, use the following command:
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
```
#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
export FZF_DEFAULT_COMMAND='
(git ls-tree -r --name-only HEAD ||
find . -path "*/\.*" -prune -o -type f -print -o -type l -print |
sed s/^..//) 2> /dev/null'
```
#### Fish shell
Fish shell before version 2.6.0 [doesn't allow](https://github.com/fish-shell/fish-shell/issues/1362)
reading from STDIN in command substitution, which means simple `vim (fzf)`
doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use
the `read` fish command:
```sh
fzf | read -l result; and vim $result
```
or, for multiple results:
```sh
fzf -m | while read -l r; set result $result $r; end; and vim $result
```
The globbing system is different in fish and thus `**` completion will not work.
However, the `CTRL-T` command will use the last token on the commandline as the
root folder for the recursive search. For instance, hitting `CTRL-T` at the end
of the following commandline
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
token on the command-line as the root directory for the recursive search. For
instance, hitting `CTRL-T` at the end of the following command-line
```sh
ls /var/
```
will list all files and folders under `/var/`.
will list all files and directories under `/var/`.
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
make use of this feature. `$dir` defaults to `.` when the last token is not a
@@ -610,4 +605,4 @@ https://github.com/junegunn/fzf/wiki/Related-projects
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Copyright (c) 2019 Junegunn Choi

View File

@@ -1,25 +1,47 @@
fzf.txt fzf Last change: November 19 2017
fzf.txt fzf Last change: November 23 2019
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
==============================================================================
FZF Vim integration
Summary
:FZF[!]
Configuration
Examples
fzf#run
fzf#wrap
fzf inside terminal buffer
Hide statusline
GVim
Tips
fzf inside terminal buffer
Starting fzf in Neovim floating window
Hide statusline
License
FZF VIM INTEGRATION *fzf-vim-integration*
==============================================================================
This repository only enables basic integration with Vim. If you're looking for
more, check out {fzf.vim}{1} project.
(Note: To use fzf in GVim, an external terminal emulator is required.)
SUMMARY *fzf-summary*
==============================================================================
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them.
1. `fzf#run([specdict])`
- Starts fzf inside Vim with the given spec
- `:callfzf#run({'source':'ls'})`
2. `fzf#wrap([specdict])->(dict)`
- Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`)
- `:echofzf#wrap({'source':'ls'})`
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
- `:callfzf#run(fzf#wrap({'source':'ls'}))`
3. `:FZF[fzf_optionsstring][pathstring]`
- Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript to
implement custom commands
- If you're looking for more such commands, check out {fzf.vim}{1} project.
The most important of all is `fzf#run`, but it would be easier to understand
the whole if we start off with `:FZF` command.
{1} https://github.com/junegunn/fzf.vim
@@ -28,8 +50,6 @@ more, check out {fzf.vim}{1} project.
==============================================================================
*:FZF*
If you have set up fzf for Vim, `:FZF` command will be added.
>
" Look for files under current directory
:FZF
@@ -37,8 +57,8 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" With fzf command-line options
:FZF --reverse --info=inline /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
@@ -56,8 +76,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
< Configuration >_____________________________________________________________~
*fzf-configuration*
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher*
*g:Fzf_launcher*
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir*
- `g:fzf_action`
- Customizable extra key bindings for opening selected files in different
@@ -68,9 +87,6 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
Examples~
@@ -102,9 +118,10 @@ Examples~
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
let g:fzf_layout = { 'window': '10new' }
" Customize fzf colors to match your color scheme
" - fzf#wrap translates this to a set of `--color` options
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
@@ -120,22 +137,64 @@ Examples~
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
" Enable per-command history
" - History files will be stored in the specified directory
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
" 'previous-history' instead of 'down' and 'up'.
let g:fzf_history_dir = '~/.local/share/fzf-history'
<
FZF#RUN *fzf#run*
FZF#RUN
==============================================================================
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
*fzf#run*
---------------------------+---------------+--------------------------------------------------------------
Option name | Type | Description ~
---------------------------+---------------+--------------------------------------------------------------
`fzf#run()` function is the core of Vim integration. It takes a single
dictionary argument, a spec, and starts fzf process accordingly. At the very
least, specify `sink` option to tell what it should do with the selected
entry.
>
call fzf#run({'sink': 'e'})
<
We haven't specified the `source`, so this is equivalent to starting fzf on
command line without standard input pipe; fzf will use find command (or
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
directory. When you select one, it will open it with the sink, `:e` command.
If you want to open it in a new tab, you can pass `:tabedit` command instead
as the sink.
>
call fzf#run({'sink': 'tabedit'})
<
Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's
equivalent to running `gitls-files|fzf` on shell.
>
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
<
fzf options can be specified as `options` entry in spec dictionary.
>
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
<
You can also pass a layout option if you don't want fzf window to take up the
entire screen.
>
" up / down / left / right / window are allowed
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
<
`source` doesn't have to be an external shell command, you can pass a Vim
array as the source. In the next example, we pass the names of color schemes
as the source to implement a color scheme selector.
>
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
\ 'fnamemodify(v:val, ":t:r")'),
\ 'sink': 'colo', 'left': '25%'})
<
The following table summarizes the available options.
---------------------------+---------------+----------------------------------------------------------------------
Option name | Type | Description ~
---------------------------+---------------+----------------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` )
`source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
@@ -143,84 +202,143 @@ following options.
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf
`dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` )
`window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `verticalaboveleft30new` )
`launcher` | string | External terminal emulator to start fzf with (GVim only)
`launcher` | funcref | Function for generating `launcher` string (GVim only)
---------------------------+---------------+--------------------------------------------------------------
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `verticalaboveleft30new` )
---------------------------+---------------+----------------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping
issues on different platforms.
should suffice, but prefer to use list type to avoid escaping issues.
>
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
<
FZF#WRAP *fzf#wrap*
FZF#WRAP
==============================================================================
`fzf#wrap([namestring,][optsdict,][fullscreenboolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
*fzf#wrap*
We have seen that several aspects of `:FZF` command can be configured with a
set of global option variables; different ways to open files (`g:fzf_action`),
window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`),
etc.
So how can we make our custom `fzf#run` calls also respect those variables?
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
`fzf#run`.
- `fzf#wrap([namestring],[specdict],[fullscreenbool])->(dict)`
- All arguments are optional. Usually we only need to pass a spec
dictionary.
- `name` is for managing history files. It is ignored if `g:fzf_history_dir`
is not defined.
- `fullscreen` can be either `0` or `1` (default: 0).
`fzf#wrap` takes a spec and returns an extended version of it (also a
dictionary) with additional options for addressing global preferences. You can
examine the return value of it like so:
>
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
echo fzf#wrap({'source': 'ls'})
<
After we "wrap" our spec, we pass it to `fzf#run`.
>
call fzf#run(fzf#wrap({'source': 'ls'}))
<
Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings and it opens fzf
window according to `g:fzf_layout` setting.
To make it easier to use, let's define `LS` command.
>
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
<
Type `:LS` and see how it works.
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
`:FZF!`. Add `-bang` to command definition, and use <bang> value to set the
last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
>
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
<
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.
>
command! -bang -complete=dir -nargs=* LS
\ 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
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 name is ignored if g:fzf_history_dir is not defined.
command! -bang -complete=dir -nargs=* LS
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
<
FZF INSIDE TERMINAL BUFFER *fzf-inside-terminal-buffer*
TIPS *fzf-tips*
==============================================================================
< fzf inside terminal buffer >________________________________________________~
*fzf-inside-terminal-buffer*
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with the non-default layout
- On Terminal Vim with a non-default layout
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}`
< Hide statusline >___________________________________________________________~
Starting fzf in Neovim floating window~
*fzf-starting-fzf-in-neovim-floating-window*
>
" Using floating windows of Neovim to start fzf
if has('nvim')
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
function! FloatingFZF()
let width = float2nr(&columns * 0.9)
let height = float2nr(&lines * 0.6)
let opts = { 'relative': 'editor',
\ 'row': (&lines - height) / 2,
\ 'col': (&columns - width) / 2,
\ 'width': width,
\ 'height': height }
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
endfunction
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
endif
<
Hide statusline~
*fzf-hide-statusline*
When fzf starts in a terminal buffer, you may want to hide the statusline of
the containing buffer.
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
the window.
For example, if you use the default layout (`{'down':'~40%'}`) on Neovim, you
might want to temporarily disable the statusline for a cleaner look.
>
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
if has('nvim') && !exists('g:fzf_layout')
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
endif
<
GVIM *fzf-gvim*
==============================================================================
With the latest version of GVim, fzf will start inside the builtin terminal
emulator of Vim. Please note that this terminal feature of Vim is still young
and unstable and you may run into some issues.
If you have an older version of GVim, you need an external terminal emulator
to start fzf with. `xterm` command is used by default, but you can customize
it with `g:fzf_launcher`.
>
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
<
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
launcher. Refer to the {this wiki page}{3} to see how to set up.
{3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
LICENSE *fzf-license*
==============================================================================
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Copyright (c) 2019 Junegunn Choi
==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

48
glide.lock generated
View File

@@ -1,48 +0,0 @@
hash: b617c76661b399f586276767bb93ee67b65dd03cfd1348ecad409e372ea97b3e
updated: 2018-06-27T18:37:20.189962-07:00
imports:
- name: github.com/codegangsta/cli
version: c6af8847eb2b7b297d07c3ede98903e95e680ef9
- name: github.com/gdamore/encoding
version: b23993cbb6353f0e6aa98d0ee318a34728f628b9
- name: github.com/gdamore/tcell
version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages:
- encoding
- name: github.com/lucasb-eyer/go-colorful
version: c900de9dbbc73129068f5af6a823068fc5f2308c
- name: github.com/Masterminds/semver
version: 15d8430ab86497c5c0da827b748823945e1cf1e1
- name: github.com/Masterminds/vcs
version: 6f1c6d150500e452704e9863f68c2559f58616bf
- name: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- name: github.com/mitchellh/go-homedir
version: b8bc1bf767474819792c23f32d8286a45736f1c6
- name: golang.org/x/crypto
version: 558b6879de74bc843225cde5686419267ff707ca
subpackages:
- ssh/terminal
- name: golang.org/x/sys
version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594
subpackages:
- unix
- name: golang.org/x/text
version: 4ee4af566555f5fbe026368b75596286a312663a
subpackages:
- encoding
- encoding/charmap
- encoding/internal
- encoding/internal/identifier
- encoding/japanese
- encoding/korean
- encoding/simplifiedchinese
- encoding/traditionalchinese
- transform
- name: gopkg.in/yaml.v2
version: 287cf08546ab5e7e37d55a84f7ed3fd1db036de5
testImports: []

View File

@@ -1,16 +0,0 @@
package: github.com/junegunn/fzf
import:
- package: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- package: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- package: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- package: github.com/gdamore/tcell
version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages:
- encoding
- package: golang.org/x/crypto
version: 558b6879de74bc843225cde5686419267ff707ca
subpackages:
- ssh/terminal

19
go.mod Normal file
View File

@@ -0,0 +1,19 @@
module github.com/junegunn/fzf
require (
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 // indirect
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 // indirect
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c
github.com/mattn/go-shellwords v1.0.3
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 // indirect
)
go 1.13

26
go.sum Normal file
View File

@@ -0,0 +1,26 @@
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI=
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ=
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df h1:tLS1QD2puA1USLvkEnGfOt+Zp2ijtNIK3z2YFaIZZR4=
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 h1:G52I+Gk/wPD4HKvKT0Vxxp9OUPxqKs3OK6rffSPtNkA=
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc=
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74 h1:/0jf0Cx3u07Ma4EzUA6NIGuvk9hb3Br6i9V8atthkwk=
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 h1:pjwO9HxObpgZBurDvTLFbSinaf3+cKpTAjVfiGaHwrI=
golang.org/x/text v0.0.0-20170530162606-4ee4af566555/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

28
install
View File

@@ -2,12 +2,11 @@
set -u
version=0.17.5
version=0.20.0
auto_completion=
key_bindings=
update_config=2
binary_arch=
allow_legacy=
shells="bash zsh fish"
prefix='~/.fzf'
prefix_expand=~/.fzf
@@ -45,7 +44,6 @@ for opt in "$@"; do
auto_completion=1
key_bindings=1
update_config=1
allow_legacy=1
;;
--xdg)
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
@@ -91,17 +89,20 @@ ask() {
check_binary() {
echo -n " - Checking fzf executable ... "
local output
output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}')
output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
elif [ "$version" != "$output" ]; then
echo "$output != $version"
binary_error="Invalid version"
else
echo "$output"
binary_error=""
return 0
output=${output/ */}
if [ "$version" != "$output" ]; then
echo "$output != $version"
binary_error="Invalid version"
else
echo "$output"
binary_error=""
return 0
fi
fi
rm -f "$fzf_base"/bin/fzf
return 1
@@ -110,7 +111,7 @@ check_binary() {
link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf"
echo " - Creating symlink: bin/fzf -> $which_fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
fi
@@ -195,6 +196,8 @@ case "$archi" in
MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
MSYS*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
MSYS*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
Windows*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
Windows*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
*) binary_available=0 binary_error=1 ;;
esac
@@ -269,7 +272,7 @@ for shell in $shells; do
# Setup fzf
# ---------
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin"
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
fi
# Auto-completion
@@ -279,7 +282,6 @@ $fzf_completion
# Key bindings
# ------------
$fzf_key_bindings
EOF
echo "OK"
done

View File

@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Oct 2018" "fzf 0.17.5" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Dec 2019" "fzf 0.20.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Copyright (c) 2019 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Oct 2018" "fzf 0.17.5" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Dec 2019" "fzf 0.20.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -70,6 +70,10 @@ Transform the presentation of each line using field index expressions
.TP
.BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.TP
.BI "--phony"
Do not perform search. With this option, fzf becomes a simple selector
interface rather than a "fuzzy finder".
.SS Search result
.TP
.B "+s, --no-sort"
@@ -79,7 +83,8 @@ Do not sort the result
Reverse the order of the input
.RS
e.g. \fBhistory | fzf --tac --no-sort\fR
e.g.
\fBhistory | fzf --tac --no-sort\fR
.RE
.TP
.BI "--tiebreak=" "CRI[,..]"
@@ -109,7 +114,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.SS Interface
.TP
.B "-m, --multi"
Enable multi-select with tab/shift-tab
Enable multi-select with tab/shift-tab. It optionally takes an integer argument
which denotes the maximum number of items that can be selected.
.TP
.B "+m, --no-multi"
Disable multi-select
@@ -118,8 +124,8 @@ Disable multi-select
Disable mouse
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the
details.
Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for
the details.
.TP
.B "--cycle"
Enable cyclic scroll
@@ -174,6 +180,11 @@ A synonym for \fB--layout=reverse\fB
.TP
.B "--border"
Draw border above and below the finder
.TP
.B "--no-unicode"
Use ASCII characters instead of Unicode box drawing characters to draw border
.TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
@@ -196,12 +207,26 @@ terminal size with \fB%\fR suffix.
.br
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
e.g.
\fBfzf --margin 10%
fzf --margin 1,5%\fR
.RE
.TP
.B "--inline-info"
Display finder info inline with the query
.BI "--info=" "STYLE"
Determines the display style of finder info.
.br
.BR default " Display on the next line to the prompt"
.br
.BR inline " Display on the same line"
.br
.BR hidden " Do not display finder info"
.br
.TP
.B "--no-info"
A synonym for \fB--info=hidden\fB
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
@@ -230,11 +255,6 @@ color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format.
.RS
e.g. \fBfzf --color=bg+:24\fR
\fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.RE
.RS
.B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16)
@@ -242,22 +262,38 @@ e.g. \fBfzf --color=bg+:24\fR
\fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
\fBfg \fRText
\fBbg \fRBackground
\fBpreview-fg \fRPreview window text
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.B EXAMPLES:
\fB# Seoul256 theme with 8-bit colors
# (https://github.com/junegunn/seoul256.vim)
fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\
--color='hl:65,fg:252,header:65,fg+:252' \\
--color='pointer:161,marker:168,prompt:110,hl+:108'
# Seoul256 theme with 24-bit colors
fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
--color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE
.TP
.B "--no-bold"
@@ -285,23 +321,41 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details).
.RS
e.g. \fBfzf --preview='head -$LINES {}'\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
e.g.
\fBfzf --preview='head -$LINES {}'
ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
fzf overrides \fB$LINES\fR and \fB$COLUMNS\fR so that they represent the exact
size of the preview window.
fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
they represent the exact size of the preview window. (It also overrides
\fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
prefix.)
A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection
was made) individually quoted.
e.g. \fBfzf --multi --preview='head -10 {+}'\fR
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
e.g.
\fBfzf --multi --preview='head -10 {+}'
git log --oneline | fzf --multi --preview 'git show {+1}'\fR
When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
Also, \fB{q}\fR is replaced to the current query string.
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may
exceed \fBARG_MAX\fR.
e.g.
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
# This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR
Note that you can escape a placeholder pattern by prepending a backslash.
@@ -309,8 +363,8 @@ Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string.
.RE
.TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with
.BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]"
Determines the layout of the preview window. If the argument contains
\fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag.
@@ -327,8 +381,9 @@ execute the command in the background.
.RE
.RS
e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR
\fBfzf --preview="file {}" --preview-window=down:1\fR
e.g.
\fBfzf --preview="head {}" --preview-window=up:30%
fzf --preview="file {}" --preview-window=down:1\fR
.RE
.SS Scripting
.TP
@@ -358,7 +413,8 @@ times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
list.
.RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
e.g.
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE
.TP
.B "--read0"
@@ -464,56 +520,110 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
e.g. \fB^core go$ | rb$ | py$\fR
.SH KEY BINDINGS
You can customize key bindings of fzf with \fB--bind\fR option which takes
a comma-separated list of key binding expressions. Each key binding expression
follows the following format: \fBKEY:ACTION\fR
.SH KEY/EVENT BINDINGS
\fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or
more \fBactions\fR. You can use it to customize key bindings or implement
dynamic behaviors.
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fB--bind\fR takes a comma-separated list of binding expressions. Each binding
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
.B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIctrl-space\fR
\fIctrl-alt-[a-z]\fR
\fIalt-[a-z]\fR
\fIalt-[0-9]\fR
\fIf[1-12]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-up\fR
\fIalt-down\fR
\fIalt-left\fR
\fIalt-right\fR
\fIalt-enter\fR
\fIalt-space\fR
\fIalt-bspace\fR (\fIalt-bs\fR)
\fIalt-/\fR
\fItab\fR
\fIbtab\fR (\fIshift-tab\fR)
\fIesc\fR
\fIdel\fR
\fIup\fR
\fIdown\fR
\fIleft\fR
\fIright\fR
\fIhome\fR
\fIend\fR
\fIpgup\fR (\fIpage-up\fR)
\fIpgdn\fR (\fIpage-down\fR)
\fIshift-up\fR
\fIshift-down\fR
\fIshift-left\fR
\fIshift-right\fR
\fIleft-click\fR
\fIright-click\fR
\fIdouble-click\fR
or any single character
e.g.
\fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
Additionally, a special event named \fIchange\fR is available which is
triggered whenever the query string is changed.
.SS AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
.br
\fIctrl-space\fR
.br
\fIctrl-\\\fR
.br
\fIctrl-]\fR
.br
\fIctrl-^\fR (\fIctrl-6\fR)
.br
\fIctrl-/\fR (\fIctrl-_\fR)
.br
\fIctrl-alt-[a-z]\fR
.br
\fIalt-[a-z]\fR
.br
\fIalt-[0-9]\fR
.br
\fIf[1-12]\fR
.br
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
.br
\fIspace\fR
.br
\fIbspace\fR (\fIbs\fR)
.br
\fIalt-up\fR
.br
\fIalt-down\fR
.br
\fIalt-left\fR
.br
\fIalt-right\fR
.br
\fIalt-enter\fR
.br
\fIalt-space\fR
.br
\fIalt-bspace\fR (\fIalt-bs\fR)
.br
\fIalt-/\fR
.br
\fItab\fR
.br
\fIbtab\fR (\fIshift-tab\fR)
.br
\fIesc\fR
.br
\fIdel\fR
.br
\fIup\fR
.br
\fIdown\fR
.br
\fIleft\fR
.br
\fIright\fR
.br
\fIhome\fR
.br
\fIend\fR
.br
\fIpgup\fR (\fIpage-up\fR)
.br
\fIpgdn\fR (\fIpage-down\fR)
.br
\fIshift-up\fR
.br
\fIshift-down\fR
.br
\fIshift-left\fR
.br
\fIshift-right\fR
.br
\fIleft-click\fR
.br
\fIright-click\fR
.br
\fIdouble-click\fR
.br
or any single character
e.g. \fBfzf --bind change:top\fR
.SS AVAILABLE EVENTS:
\fIchange\fR (triggered whenever the query string is changed)
.br
e.g.
\fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed
fzf --bind change:top\fR
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
@@ -524,11 +634,13 @@ triggered whenever the query string is changed.
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clears query string if not empty, aborts fzf otherwise)
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR
\fBdeselect-all\fR (deselect all matches)
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
@@ -552,10 +664,11 @@ triggered whenever the query string is changed.
\fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect-all\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR
\fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
@@ -569,9 +682,14 @@ triggered whenever the query string is changed.
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
e.g.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
.SS COMMAND EXECUTION
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
@@ -600,9 +718,9 @@ parse errors.
\fBexecute|...|\fR
\fBexecute:...\fR
.RS
This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list of key-action pairs.
The last one is the special form that frees you from parse errors as it does
not expect the closing character. The catch is that it should be the last one
in the comma-separated list of key-action pairs.
.RE
fzf switches to the alternate screen when executing a command. However, if the
@@ -612,6 +730,26 @@ executes the command without the switching. Note that fzf will not be
responsive until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR).
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list
without restarting fzf. It takes the same command template with placeholder
expressions as \fBexecute(...)\fR.
See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info.
e.g.
\fB# Update the list of processes by pressing CTRL-R
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\
--header-lines=1 --layout=reverse
# Integration with ripgrep
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="foobar"
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --phony --query "$INITIAL_QUERY"\fR
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -49,10 +49,18 @@ if s:is_win
" Use utf-8 for fzf.vim commands
" Return array of shell commands for cmd.exe
let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0)
function! s:enc_to_cp(str)
return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction
function! s:wrap_cmds(cmds)
return map(['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] +
\ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) +
\ ['chcp %origchcp% > nul'], 'v:val."\r"')
return map([
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
\ + ['endlocal'],
\ '<SID>enc_to_cp(v:val."\r")')
endfunction
else
let s:term_marker = ";#FZF"
@@ -64,6 +72,10 @@ else
function! s:wrap_cmds(cmds)
return a:cmds
endfunction
function! s:enc_to_cp(str)
return a:str
endfunction
endif
function! s:shellesc_cmd(arg)
@@ -75,7 +87,7 @@ function! s:shellesc_cmd(arg)
endfunction
function! fzf#shellescape(arg, ...)
let shell = get(a:000, 0, &shell)
let shell = get(a:000, 0, s:is_win ? 'cmd.exe' : 'sh')
if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg)
endif
@@ -239,7 +251,7 @@ function! s:common_sink(action, lines) abort
endfunction
function! s:get_color(attr, ...)
let gui = has('termguicolors') && &termguicolors
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000
@@ -254,7 +266,7 @@ endfunction
function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors)
return empty(colors) ? '' : fzf#shellescape('--color='.colors)
endfunction
function! s:validate_layout(layout)
@@ -334,19 +346,21 @@ function! fzf#wrap(...)
endfunction
function! s:use_sh()
let [shell, shellslash] = [&shell, &shellslash]
let [shell, shellslash, shellcmdflag, shellxquote] = [&shell, &shellslash, &shellcmdflag, &shellxquote]
if s:is_win
set shell=cmd.exe
set noshellslash
let &shellcmdflag = has('nvim') ? '/s /c' : '/c'
let &shellxquote = has('nvim') ? '"' : '('
else
set shell=sh
endif
return [shell, shellslash]
return [shell, shellslash, shellcmdflag, shellxquote]
endfunction
function! fzf#run(...) abort
try
let [shell, shellslash] = s:use_sh()
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': s:fzf_tempname() }
@@ -377,7 +391,7 @@ try
let prefix = '( '.source.' )|'
elseif type == 3
let temps.input = s:fzf_tempname()
call writefile(source, temps.input)
call writefile(map(source, '<SID>enc_to_cp(v:val)'), temps.input)
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
else
throw 'Invalid source type'
@@ -416,7 +430,7 @@ try
call s:callback(dict, lines)
return lines
finally
let [&shell, &shellslash] = [shell, shellslash]
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry
endfunction
@@ -457,7 +471,8 @@ function! s:pushd(dict)
let cwd = s:fzf_getcwd()
let w:fzf_pushd = {
\ 'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'),
\ 'origin': cwd
\ 'origin': cwd,
\ 'bufname': bufname('')
\ }
execute 'lcd' s:escape(a:dict.dir)
let cwd = s:fzf_getcwd()
@@ -493,19 +508,19 @@ function! s:dopopd()
" matches 'dir' entry. However, it is possible that the sink function did
" change the directory to 'dir'. In that case, the user will have an
" unexpected result.
if s:fzf_getcwd() ==# w:fzf_pushd.dir
if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname(''))
execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin)
endif
unlet w:fzf_pushd
endfunction
function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
let fmt = 'xterm -T "[fzf]" -bg "%s" -fg "%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim')
let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
endif
return printf(fmt,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
\ escape(synIDattr(hlID("Normal"), "bg"), '#'), escape(synIDattr(hlID("Normal"), "fg"), '#'),
\ &columns, &lines/2, getwinposx(), getwinposy())
endfunction
unlet! s:launcher
@@ -518,6 +533,10 @@ endif
function! s:exit_handler(code, command, ...)
if a:code == 130
return 0
elseif has('nvim') && a:code == 129
" When deleting the terminal buffer while fzf is still running,
" Nvim sends SIGHUP.
return 0
elseif a:code > 1
call s:error('Error running ' . a:command)
if !empty(a:000)
@@ -606,8 +625,9 @@ function! s:calc_size(max, val, dict)
let srcsz = len(a:dict.source)
endif
let opts = s:evaluate_opts(get(a:dict, 'options', '')).$FZF_DEFAULT_OPTS
let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', ''))
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction

View File

@@ -9,12 +9,14 @@
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
@@ -22,7 +24,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
@@ -121,11 +123,11 @@ _fzf_handle_dynamic_completion() {
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then
orig_complete=$(complete -p "$cmd" 2> /dev/null)
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@"
ret=$?
# _completion_loader may not have updated completion for the command
if [ "$(complete -p "$cmd" 2> /dev/null)" != "$orig_complete" ]; then
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
eval "$(complete | command grep " -F.* $orig_cmd$" | __fzf_orig_completion_filter)"
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
@@ -193,12 +195,13 @@ _fzf_complete() {
selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
printf '\e[5n'
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
return 0
else
COMPREPLY=("$cur")
fi
printf '\e[5n'
return 0
else
shift
_fzf_handle_dynamic_completion "$cmd" "$@"
@@ -241,7 +244,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
@@ -290,7 +293,7 @@ if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1
fi
_fzf_defc() {
__fzf_defc() {
local cmd func opts orig_var orig def
cmd="$1"
func="$2"
@@ -307,16 +310,14 @@ _fzf_defc() {
# Anything
for cmd in $a_cmds; do
_fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
__fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
done
# Directory
for cmd in $d_cmds; do
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done
unset _fzf_defc
# Kill completion
complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill
@@ -330,3 +331,23 @@ complete -F _fzf_complete_export -o default -o bashdefault export
complete -F _fzf_complete_unalias -o default -o bashdefault unalias
unset cmd d_cmds a_cmds x_cmds
_fzf_setup_completion() {
local kind fn cmd
kind=$1
fn=_fzf_${1}_completion
if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then
echo "usage: ${FUNCNAME[0]} path|dir COMMANDS..."
return 1
fi
shift
for cmd in "$@"; do
eval "$(complete -p "$cmd" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
case "$kind" in
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
esac
done
}
fi

View File

@@ -9,12 +9,14 @@
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
@@ -22,7 +24,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
@@ -112,7 +114,8 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <(
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host ' | command grep -v '[*?]' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
setopt localoptions nonomatch
command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
@@ -155,6 +158,12 @@ fzf-completion() {
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]}${tokens[-1]}"
tokens=(${tokens[0,-2]})
fi
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
@@ -192,3 +201,5 @@ fzf-completion() {
zle -N fzf-completion
bindkey '^I' fzf-completion
fi

View File

@@ -55,7 +55,7 @@ __fzf_history__() (
local line
shopt -u nocaseglob nocasematch
line=$(
HISTTIMEFORMAT= history |
HISTTIMEFORMAT= builtin history |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac --sync -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then

View File

@@ -47,7 +47,7 @@ function fzf_key_bindings
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
else
history | eval (__fzfcmd) -q '(commandline)' | read -l result

View File

@@ -8,7 +8,7 @@ __fsel() {
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
setopt localoptions pipefail no_aliases 2> /dev/null
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} "
done
@@ -49,13 +49,14 @@ zle -N fzf-redraw-prompt
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 \
-o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
if [[ -z "$dir" ]]; then
zle redisplay
return 0
fi
cd "$dir"
unset dir # ensure this doesn't end up appearing in prompt expansion
local ret=$?
zle fzf-redraw-prompt
return $ret
@@ -66,7 +67,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected=( $(fc -rl 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$?

View File

@@ -371,7 +371,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M)
// Rune array
offset32, T := alloc32(offset32, slab, N)
_, T := alloc32(offset32, slab, N)
input.CopyRunes(T)
// Phase 2. Calculate bonus for each point
@@ -453,7 +453,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
copy(H, H0[f0:lastIdx+1])
// Possible length of consecutive chunk at each position.
offset16, C := alloc16(offset16, slab, width*M)
_, C := alloc16(offset16, slab, width*M)
copy(C, C0[f0:lastIdx+1])
Fsub := F[1:]

View File

@@ -32,6 +32,55 @@ func (s *ansiState) equals(t *ansiState) bool {
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
}
func (s *ansiState) ToString() string {
if !s.colored() {
return ""
}
ret := ""
if s.attr&tui.Bold > 0 {
ret += "1;"
}
if s.attr&tui.Dim > 0 {
ret += "2;"
}
if s.attr&tui.Italic > 0 {
ret += "3;"
}
if s.attr&tui.Underline > 0 {
ret += "4;"
}
if s.attr&tui.Blink > 0 {
ret += "5;"
}
if s.attr&tui.Reverse > 0 {
ret += "7;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
}
func toAnsiString(color tui.Color, offset int) string {
col := int(color)
ret := ""
if col == -1 {
ret += strconv.Itoa(offset + 9)
} else if col < 8 {
ret += strconv.Itoa(offset + col)
} else if col < 16 {
ret += strconv.Itoa(offset - 30 + 90 + col - 8)
} else if col < 256 {
ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col)
} else if col >= (1 << 24) {
r := strconv.Itoa((col >> 16) & 0xff)
g := strconv.Itoa((col >> 8) & 0xff)
b := strconv.Itoa(col & 0xff)
ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b
}
return ret + ";"
}
var ansiRegex *regexp.Regexp
func init() {

View File

@@ -2,6 +2,7 @@ package fzf
import (
"fmt"
"strings"
"testing"
"github.com/junegunn/fzf/src/tui"
@@ -156,3 +157,31 @@ func TestExtractColor(t *testing.T) {
assert((*offsets)[1], 6, 11, 200, 100, false)
})
}
func TestAnsiCodeStringConversion(t *testing.T) {
assert := func(code string, prevState *ansiState, expected string) {
state := interpretCode(code, prevState)
if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
}
}
assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m")
assert("\x1b[92m", nil, "\x1b[92;49m")
assert("\x1b[102m", nil, "\x1b[39;102m")
assert("\x1b[31m", &ansiState{fg: 4, bg: 4}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse}, "\x1b[1;2;7;31;49m")
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;7m",
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
}

View File

@@ -64,6 +64,13 @@ func (cl *ChunkList) Push(data []byte) bool {
return ret
}
// Clear clears the data
func (cl *ChunkList) Clear() {
cl.mutex.Lock()
cl.chunks = nil
cl.mutex.Unlock()
}
// Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock()

View File

@@ -1,6 +1,7 @@
package fzf
import (
"math"
"os"
"time"
@@ -9,7 +10,7 @@ import (
const (
// Current version
version = "0.17.5"
version = "0.20.0"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -27,6 +28,7 @@ const (
spinnerDuration = 200 * time.Millisecond
previewCancelWait = 500 * time.Millisecond
maxPatternLength = 300
maxMulti = math.MaxInt32
// Matcher
numPartitionsMultiplier = 8

View File

@@ -63,12 +63,14 @@ func Run(opts *Options, revision string) {
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil
}
var lineAnsiState, prevLineAnsiState *ansiState
if opts.Ansi {
if opts.Theme != nil {
var state *ansiState
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars([]byte(trimmed)), offsets
}
} else {
@@ -100,6 +102,22 @@ func Run(opts *Options, revision string) {
} else {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(string(data), opts.Delimiter)
if opts.Ansi && opts.Theme != nil && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
ansiStateDup := *prevLineAnsiState
ansiState = &ansiStateDup
}
for _, token := range tokens {
prevAnsiState := ansiState
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
if prevAnsiState != nil {
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
} else {
token.text.Prepend("\x1b[m")
}
}
}
trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines {
@@ -108,6 +126,7 @@ func Run(opts *Options, revision string) {
return false
}
item.text, item.colors = ansiProcessor([]byte(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
itemIndex++
@@ -117,10 +136,11 @@ func Run(opts *Options, revision string) {
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader := NewReader(func(data []byte) bool {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero)
}, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource()
}
@@ -149,6 +169,7 @@ func Run(opts *Options, revision string) {
}
pattern := patternBuilder([]rune(*opts.Filter))
matcher.sort = pattern.sortable
found := false
if streamingFilter {
@@ -163,7 +184,7 @@ func Run(opts *Options, revision string) {
}
}
return false
}, eventBox, opts.ReadZero)
}, eventBox, opts.ReadZero, false)
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
@@ -203,11 +224,28 @@ func Run(opts *Options, revision string) {
// Event coordination
reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0
var nextCommand *string
restart := func(command string) {
reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear()
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command)
}
eventBox.Watch(EvtReadNew)
for {
delay := true
ticks++
input := func() []rune {
if opts.Phony {
return []rune{}
}
return []rune(terminal.Input())
}
eventBox.Wait(func(events *util.Events) {
if _, fin := (*events)[EvtReadFin]; fin {
delete(*events, EvtReadNew)
@@ -216,21 +254,39 @@ func Run(opts *Options, revision string) {
switch evt {
case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(bool))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand)
nextCommand = nil
break
} else {
reading = reading && evt == EvtReadNew
}
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading, value.(*string))
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
}
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
case EvtSearchNew:
var command *string
switch val := value.(type) {
case bool:
sort = val
case searchRequest:
sort = val.sort
command = val.command
}
if command != nil {
if reading {
reader.terminate()
nextCommand = command
} else {
restart(*command)
}
break
}
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
delay = false
case EvtSearchProgress:
@@ -240,7 +296,9 @@ func Run(opts *Options, revision string) {
}
case EvtHeader:
terminal.UpdateHeader(value.([]string))
headerPadded := make([]string, opts.HeaderLines)
copy(headerPadded, value.([]string))
terminal.UpdateHeader(headerPadded)
case EvtSearchFin:
switch val := value.(type) {
@@ -270,7 +328,7 @@ func Run(opts *Options, revision string) {
terminal.startChan <- true
}
}
terminal.UpdateList(val)
terminal.UpdateList(val, clearSelection())
}
}
}

View File

@@ -59,7 +59,7 @@ func (h *History) append(line string) error {
lines := append(h.lines[:len(h.lines)-1], line)
if len(lines) > h.maxSize {
lines = lines[len(lines)-h.maxSize : len(lines)]
lines = lines[len(lines)-h.maxSize:]
}
h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)

View File

@@ -12,10 +12,11 @@ import (
// MatchRequest represents a search request
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
final bool
sort bool
chunks []*Chunk
pattern *Pattern
final bool
sort bool
clearCache bool
}
// Matcher is responsible for performing search
@@ -69,7 +70,7 @@ func (m *Matcher) Loop() {
events.Clear()
})
if request.sort != m.sort {
if request.sort != m.sort || request.clearCache {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
@@ -207,13 +208,13 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return nil, wait()
}
if time.Now().Sub(startedAt) > progressMinDuration {
if time.Since(startedAt) > progressMinDuration {
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
}
}
partialResults := make([][]Result, numSlices)
for _ = range slices {
for range slices {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
@@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
// Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
pattern := m.patternBuilder(patternRunes)
var event util.EventType
@@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else {
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
}

View File

@@ -33,12 +33,13 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
--phony Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied [length|begin|end|index]
(default: length)
Interface
-m, --multi Enable multi-select with tab/shift-tab
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--cycle Enable cyclic scroll
@@ -56,7 +57,7 @@ const usage = `usage: fzf [options]
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--border Draw border above and below the finder
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query
--info=STYLE Finder info style [default|inline|hidden]
--prompt=STR Input prompt (default: '> ')
--header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header
@@ -141,12 +142,21 @@ const (
layoutReverseList
)
type infoStyle int
const (
infoDefault infoStyle = iota
infoInline
infoHidden
)
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
wrap bool
border bool
}
// Options stores the values of command-line options
@@ -154,6 +164,7 @@ type Options struct {
Fuzzy bool
FuzzyAlgo algo.Algo
Extended bool
Phony bool
Case Case
Normalize bool
Nth []Range
@@ -162,7 +173,7 @@ type Options struct {
Sort int
Tac bool
Criteria []criterion
Multi bool
Multi int
Ansi bool
Mouse bool
Theme *tui.ColorTheme
@@ -175,7 +186,7 @@ type Options struct {
Hscroll bool
HscrollOff int
FileWord bool
InlineInfo bool
InfoStyle infoStyle
JumpLabels string
Prompt string
Query string
@@ -189,12 +200,14 @@ type Options struct {
PrintQuery bool
ReadZero bool
Printer func(string)
PrintSep string
Sync bool
History *History
Header []string
HeaderLines int
Margin [4]sizeSpec
Bordered bool
Unicode bool
Tabstop int
ClearOnExit bool
Version bool
@@ -205,6 +218,7 @@ func defaultOptions() *Options {
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true,
Phony: false,
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
@@ -213,7 +227,7 @@ func defaultOptions() *Options {
Sort: 1000,
Tac: false,
Criteria: []criterion{byScore, byLength},
Multi: false,
Multi: 0,
Ansi: false,
Mouse: true,
Theme: tui.EmptyTheme(),
@@ -225,7 +239,7 @@ func defaultOptions() *Options {
Hscroll: true,
HscrollOff: 10,
FileWord: false,
InlineInfo: false,
InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
Prompt: "> ",
Query: "",
@@ -235,22 +249,24 @@ func defaultOptions() *Options {
ToggleSort: false,
Expect: make(map[int]string),
Keymap: make(map[int][]action),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true},
PrintQuery: false,
ReadZero: false,
Printer: func(str string) { fmt.Println(str) },
PrintSep: "\n",
Sync: false,
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
Margin: defaultMargin(),
Unicode: true,
Tabstop: 8,
ClearOnExit: true,
Version: false}
}
func help(code int) {
os.Stderr.WriteString(usage)
os.Stdout.WriteString(usage)
os.Exit(code)
}
@@ -310,13 +326,14 @@ func nextInt(args []string, i *int, message string) int {
return atoi(args[*i])
}
func optionalNumeric(args []string, i *int) int {
func optionalNumeric(args []string, i *int, defaultValue int) int {
if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 {
*i++
return atoi(args[*i])
}
}
return 1 // Don't care
return defaultValue
}
func splitNth(str string) []Range {
@@ -381,7 +398,7 @@ func parseKeyChords(str string, message string) map[int]string {
}
tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
tokens = append(tokens, ",")
}
@@ -409,6 +426,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.BSpace
case "ctrl-space":
chord = tui.CtrlSpace
case "ctrl-^", "ctrl-6":
chord = tui.CtrlCaret
case "ctrl-/", "ctrl-_":
chord = tui.CtrlSlash
case "ctrl-\\":
chord = tui.CtrlBackSlash
case "ctrl-]":
chord = tui.CtrlRightBracket
case "change":
chord = tui.Change
case "alt-enter", "alt-return":
@@ -572,10 +597,16 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme.Fg = ansi
case "bg":
theme.Bg = ansi
case "preview-fg":
theme.PreviewFg = ansi
case "preview-bg":
theme.PreviewBg = ansi
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "gutter":
theme.Gutter = ansi
case "hl":
theme.Match = ansi
case "hl+":
@@ -621,13 +652,19 @@ func init() {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
"(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
`(?si)[:+](execute(?:-multi|-silent)?|reload):.+|[:+](execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
}
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
prefix := ":execute"
if src[len(prefix)] == '-' {
symbol := ":"
if strings.HasPrefix(src, "+") {
symbol = "+"
}
prefix := symbol + "execute"
if strings.HasPrefix(src[1:], "reload") {
prefix = symbol + "reload"
} else if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
@@ -705,6 +742,10 @@ func parseKeymap(keymap map[int][]action, str string) {
appendAction(actEndOfLine)
case "cancel":
appendAction(actCancel)
case "clear-query":
appendAction(actClearQuery)
case "clear-selection":
appendAction(actClearSelection)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
@@ -776,10 +817,16 @@ func parseKeymap(keymap map[int][]action, str string) {
default:
t := isExecuteAction(specLower)
if t == actIgnore {
errorExit("unknown action: " + spec)
if specIndex == 0 && specLower == "" {
actions = append(keymap[key], actions...)
} else {
errorExit("unknown action: " + spec)
}
} else {
var offset int
switch t {
case actReload:
offset = len("reload")
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
@@ -815,6 +862,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2]
}
switch prefix {
case "reload":
return actReload
case "execute":
return actExecute
case "execute-silent":
@@ -880,6 +929,20 @@ func parseLayout(str string) layoutType {
return layoutDefault
}
func parseInfoStyle(str string) infoStyle {
switch str {
case "default":
return infoDefault
case "inline":
return infoInline
case "hidden":
return infoHidden
default:
errorExit("invalid info style (expected: default / inline / hidden)")
}
return infoDefault
}
func parsePreviewWindow(opts *previewOpts, input string) {
// Default
opts.position = posRight
@@ -891,6 +954,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
for _, token := range tokens {
switch token {
case "":
case "hidden":
opts.hidden = true
case "wrap":
@@ -903,6 +967,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.position = posLeft
case "right":
opts.position = posRight
case "border":
opts.border = true
case "noborder":
opts.border = false
default:
if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
@@ -1007,6 +1075,10 @@ func parseOptions(opts *Options, allArgs []string) {
}
case "--no-expect":
opts.Expect = make(map[int]string)
case "--no-phony":
opts.Phony = false
case "--phony":
opts.Phony = true
case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
@@ -1027,7 +1099,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--with-nth":
opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
case "-s", "--sort":
opts.Sort = optionalNumeric(allArgs, &i)
opts.Sort = optionalNumeric(allArgs, &i, 1)
case "+s", "--no-sort":
opts.Sort = 0
case "--tac":
@@ -1039,9 +1111,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "+i":
opts.Case = CaseRespect
case "-m", "--multi":
opts.Multi = true
opts.Multi = optionalNumeric(allArgs, &i, maxMulti)
case "+m", "--no-multi":
opts.Multi = false
opts.Multi = 0
case "--ansi":
opts.Ansi = true
case "--no-ansi":
@@ -1081,10 +1153,15 @@ func parseOptions(opts *Options, allArgs []string) {
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--info":
opts.InfoStyle = parseInfoStyle(
nextString(allArgs, &i, "info style required"))
case "--no-info":
opts.InfoStyle = infoHidden
case "--inline-info":
opts.InlineInfo = true
opts.InfoStyle = infoInline
case "--no-inline-info":
opts.InlineInfo = false
opts.InfoStyle = infoDefault
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
@@ -1102,8 +1179,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ReadZero = false
case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") }
opts.PrintSep = "\x00"
case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) }
opts.PrintSep = "\n"
case "--print-query":
opts.PrintQuery = true
case "--no-print-query":
@@ -1137,7 +1216,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]"))
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]"))
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height":
@@ -1150,6 +1229,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Bordered = false
case "--border":
opts.Bordered = true
case "--no-unicode":
opts.Unicode = false
case "--unicode":
opts.Unicode = true
case "--margin":
opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
@@ -1178,12 +1261,16 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "-m", "--multi="); match {
opts.Multi = atoi(value)
} else if match, value := optString(arg, "--height="); match {
opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--min-height="); match {
opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--layout="); match {
opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {

View File

@@ -243,9 +243,10 @@ func TestBind(t *testing.T) {
check(tui.CtrlA, "", actBeginningOfLine)
parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+top,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA, "", actKillLine)
check(tui.CtrlB, "", actToggleSort, actUp, actDown)
@@ -253,7 +254,7 @@ func TestBind(t *testing.T) {
check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort)
check(tui.F1, "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actTop, actTop)
check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute)

View File

@@ -52,6 +52,7 @@ type Pattern struct {
forward bool
text []rune
termSets []termSet
sortable bool
cacheable bool
cacheKey string
delimiter Delimiter
@@ -101,18 +102,27 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
}
caseSensitive := true
sortable := true
termSets := []termSet{}
if extended {
termSets = parseTerms(fuzzy, caseMode, normalize, asString)
// We should not sort the result if there are only inverse search terms
sortable = false
Loop:
for _, termSet := range termSets {
for idx, term := range termSet {
if !term.inv {
sortable = true
}
// If the query contains inverse search terms or OR operators,
// we cannot cache the search scope
if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
cacheable = false
break Loop
if sortable {
// Can't break until we see at least one non-inverse term
break Loop
}
}
}
}
@@ -134,6 +144,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
forward: forward,
text: []rune(asString),
termSets: termSets,
sortable: sortable,
cacheable: cacheable,
nth: nth,
delimiter: delimiter,

View File

@@ -4,6 +4,8 @@ import (
"bufio"
"io"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
@@ -16,11 +18,17 @@ type Reader struct {
eventBox *util.EventBox
delimNil bool
event int32
finChan chan bool
mutex sync.Mutex
exec *exec.Cmd
command *string
killed bool
wait bool
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady)}
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@@ -29,9 +37,12 @@ func (r *Reader) startEventPoller() {
pollInterval := readerPollIntervalMin
for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
r.eventBox.Set(EvtReadNew, true)
r.eventBox.Set(EvtReadNew, (*string)(nil))
pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
if r.wait {
r.finChan <- true
}
return
} else {
pollInterval += readerPollIntervalStep
@@ -46,7 +57,37 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin))
r.eventBox.Set(EvtReadFin, success)
if r.wait {
<-r.finChan
}
r.mutex.Lock()
ret := r.command
if success || r.killed {
ret = nil
}
r.mutex.Unlock()
r.eventBox.Set(EvtReadFin, ret)
}
func (r *Reader) terminate() {
r.mutex.Lock()
defer func() { r.mutex.Unlock() }()
r.killed = true
if r.exec != nil && r.exec.Process != nil {
util.KillCommand(r.exec)
} else {
os.Stdin.Close()
}
}
func (r *Reader) restart(command string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(nil, command)
r.fin(success)
}
// ReadSource reads data from the default command or from standard input
@@ -54,12 +95,13 @@ func (r *Reader) ReadSource() {
r.startEventPoller()
var success bool
if util.IsTty() {
// The default command for *nix requires bash
shell := "bash"
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
// The default command for *nix requires bash
success = r.readFromCommand("bash", defaultCommand)
success = r.readFromCommand(&shell, defaultCommand)
} else {
success = r.readFromCommand("sh", cmd)
success = r.readFromCommand(nil, cmd)
}
} else {
success = r.readFromStdin()
@@ -102,16 +144,25 @@ func (r *Reader) readFromStdin() bool {
return true
}
func (r *Reader) readFromCommand(shell string, cmd string) bool {
listCommand := util.ExecCommandWith(shell, cmd, false)
out, err := listCommand.StdoutPipe()
func (r *Reader) readFromCommand(shell *string, command string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
if shell != nil {
r.exec = util.ExecCommandWith(*shell, command, true)
} else {
r.exec = util.ExecCommand(command, true)
}
out, err := r.exec.StdoutPipe()
if err != nil {
r.mutex.Unlock()
return false
}
err = listCommand.Start()
err = r.exec.Start()
r.mutex.Unlock()
if err != nil {
return false
}
r.feed(out)
return listCommand.Wait() == nil
return r.exec.Wait() == nil
}

View File

@@ -10,10 +10,9 @@ import (
func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
reader := Reader{
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb,
event: int32(EvtReady)}
reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true },
eb, false, true)
reader.startEventPoller()
@@ -23,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Normal command
reader.fin(reader.readFromCommand("sh", `echo abc && echo def`))
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs)
}
@@ -48,7 +47,7 @@ func TestReadFromCommand(t *testing.T) {
reader.startEventPoller()
// Failing command
reader.fin(reader.readFromCommand("sh", `no-such-command`))
reader.fin(reader.readFromCommand(nil, `no-such-command`))
strs = []string{}
if len(strs) > 0 {
t.Errorf("%s", strs)

View File

@@ -16,7 +16,6 @@ type colorOffset struct {
offset [2]int32
color tui.ColorPair
attr tui.Attr
index int32
}
type Result struct {

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"regexp"
@@ -22,9 +23,11 @@ import (
// import "github.com/pkg/profile"
var placeholder *regexp.Regexp
var activeTempFiles []string
func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q})")
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
activeTempFiles = []string{}
}
type jumpMode int
@@ -40,6 +43,7 @@ type previewer struct {
lines int
offset int
enabled bool
more bool
}
type itemLine struct {
@@ -56,7 +60,7 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
inlineInfo bool
infoStyle infoStyle
prompt string
promptLen int
queryLen [2]int
@@ -72,7 +76,7 @@ type Terminal struct {
xoffset int
yanked []rune
input []rune
multi bool
multi int
sort bool
toggleSort bool
delimiter Delimiter
@@ -88,6 +92,7 @@ type Terminal struct {
tabstop int
margin [4]sizeSpec
strong tui.Attr
unicode bool
bordered bool
cleanExit bool
border tui.Window
@@ -97,10 +102,11 @@ type Terminal struct {
count int
progress int
reading bool
success bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
@@ -179,6 +185,8 @@ const (
actBackwardWord
actCancel
actClearScreen
actClearQuery
actClearSelection
actDeleteChar
actDeleteCharEOF
actEndOfLine
@@ -222,12 +230,20 @@ const (
actExecuteMulti // Deprecated
actSigStop
actTop
actReload
)
type placeholderFlags struct {
plus bool
preserveSpace bool
number bool
query bool
file bool
}
type searchRequest struct {
sort bool
command *string
}
func toActions(types ...actionType) []action {
@@ -347,7 +363,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
effectiveMinHeight *= 2
}
if opts.InlineInfo {
if opts.InfoStyle != infoDefault {
effectiveMinHeight -= 1
}
if opts.Bordered {
@@ -366,7 +382,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
t := Terminal{
initDelay: delay,
inlineInfo: opts.InlineInfo,
infoStyle: opts.InfoStyle,
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
@@ -390,6 +406,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
unicode: opts.Unicode,
bordered: opts.Bordered,
cleanExit: opts.ClearOnExit,
strong: strongAttr,
@@ -399,15 +416,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
success: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
preview: opts.Preview,
previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
@@ -422,6 +440,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
return &t
}
func (t *Terminal) noInfoLine() bool {
return t.infoStyle != infoDefault
}
// Input returns current query string
func (t *Terminal) Input() []rune {
t.mutex.Lock()
@@ -430,11 +452,11 @@ func (t *Terminal) Input() []rune {
}
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.success = success
t.failed = failedCommand
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
if final {
@@ -473,10 +495,13 @@ func (t *Terminal) UpdateProgress(progress float32) {
}
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.mutex.Lock()
t.progress = 100
t.merger = merger
if reset {
t.selected = make(map[int32]selectedItem)
}
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
@@ -599,11 +624,17 @@ func (t *Terminal) resizeWindows() {
marginInt[0]-1,
marginInt[3],
width,
height+2, tui.BorderHorizontal)
height+2,
false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode))
}
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) {
t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround)
previewBorder := tui.MakeBorderStyle(tui.BorderAround, t.unicode)
if !t.preview.border {
previewBorder = tui.MakeTransparentBorder()
}
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
pwidth := w - 4
// ncurses auto-wraps the line when the cursor reaches the right-end of
// the window. To prevent unintended line-wraps, we use the width one
@@ -611,29 +642,28 @@ func (t *Terminal) resizeWindows() {
if !t.preview.wrap && t.tui.DoesAutoWrap() {
pwidth += 1
}
t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone)
os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2))
t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, true, noBorder)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone)
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone)
marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone)
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone)
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
@@ -641,7 +671,7 @@ func (t *Terminal) resizeWindows() {
marginInt[0],
marginInt[3],
width,
height, tui.BorderNone)
height, false, noBorder)
}
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
@@ -656,7 +686,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
y = h - y - 1
case layoutReverseList:
n := 2 + len(t.header)
if t.inlineInfo {
if t.noInfoLine() {
n--
}
if y < n {
@@ -709,7 +739,17 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() {
pos := 0
if t.inlineInfo {
switch t.infoStyle {
case infoDefault:
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
}
t.move(1, 2, false)
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() {
return
@@ -721,18 +761,13 @@ func (t *Terminal) printInfo() {
t.window.CPrint(tui.ColPrompt, t.strong, " < ")
}
pos += len(" < ")
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
}
t.move(1, 2, false)
pos = 2
case infoHidden:
return
}
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
if t.toggleSort {
if t.sort {
output += " +S"
@@ -740,22 +775,25 @@ func (t *Terminal) printInfo() {
output += " -S"
}
}
if t.multi && len(t.selected) > 0 {
output += fmt.Sprintf(" (%d)", len(t.selected))
if len(t.selected) > 0 {
if t.multi == maxMulti {
output += fmt.Sprintf(" (%d)", len(t.selected))
} else {
output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
}
}
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
if !t.success && t.count == 0 {
if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 {
output = "[$FZF_DEFAULT_COMMAND failed]"
} else {
output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
}
if t.failed != nil && t.count == 0 {
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
}
if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output)
maxWidth := t.window.Width() - pos
if len(output) > maxWidth {
outputRunes, _ := t.trimRight([]rune(output), maxWidth-2)
output = string(outputRunes) + ".."
}
t.window.CPrint(tui.ColInfo, 0, output)
}
func (t *Terminal) printHeader() {
@@ -766,7 +804,7 @@ func (t *Terminal) printHeader() {
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
if t.inlineInfo {
if t.noInfoLine() {
line--
}
if line >= max {
@@ -795,7 +833,7 @@ func (t *Terminal) printList() {
i = maxy - 1 - j
}
line := i + 2 + len(t.header)
if t.inlineInfo {
if t.noInfoLine() {
line--
}
if i < count {
@@ -834,15 +872,16 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
}
t.move(line, 0, false)
t.window.CPrint(tui.ColCursor, t.strong, label)
if current {
t.window.CPrint(tui.ColCurrentCursor, t.strong, label)
if selected {
t.window.CPrint(tui.ColSelected, t.strong, ">")
t.window.CPrint(tui.ColCurrentSelected, t.strong, ">")
} else {
t.window.CPrint(tui.ColCurrent, t.strong, " ")
t.window.CPrint(tui.ColCurrentSelected, t.strong, " ")
}
newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
} else {
t.window.CPrint(tui.ColCursor, t.strong, label)
if selected {
t.window.CPrint(tui.ColSelected, t.strong, ">")
} else {
@@ -997,19 +1036,6 @@ func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.Color
return displayWidth
}
func numLinesMax(str string, max int) int {
lines := 0
for lines < max {
idx := strings.Index(str, "\n")
if idx < 0 {
break
}
str = str[idx+1:]
lines++
}
return lines
}
func (t *Terminal) printPreview() {
if !t.hasPreviewWindow() {
return
@@ -1023,36 +1049,37 @@ func (t *Terminal) printPreview() {
reader := bufio.NewReader(strings.NewReader(t.previewer.text))
lineNo := -t.previewer.offset
height := t.pwindow.Height()
t.previewer.more = t.previewer.offset > 0
var ansi *ansiState
for {
for ; ; lineNo++ {
line, err := reader.ReadString('\n')
eof := err == io.EOF
if !eof {
line = line[:len(line)-1]
}
lineNo++
if lineNo > height ||
t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
break
} else if lineNo > 0 {
} else if lineNo >= 0 {
var fillRet tui.FillReturn
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
if !t.preview.wrap {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, _ = t.processTabs(trimmed, 0)
str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width
if t.theme != nil && ansi != nil && ansi.colored() {
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str)
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
return fillRet == tui.FillContinue
})
switch fillRet {
case tui.FillNextLine:
t.previewer.more = t.previewer.more || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
if fillRet == tui.FillNextLine {
continue
case tui.FillSuspend:
} else if fillRet == tui.FillSuspend {
break
}
t.pwindow.Fill("\n")
@@ -1063,6 +1090,7 @@ func (t *Terminal) printPreview() {
}
t.pwindow.FinishFill()
if t.previewer.lines > height {
t.previewer.more = true
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
pos := t.pwindow.Width() - len(offset)
if t.tui.DoesAutoWrap() {
@@ -1098,6 +1126,7 @@ func (t *Terminal) printAll() {
}
func (t *Terminal) refresh() {
t.placeCursor()
if !t.suppress {
windows := make([]tui.Window, 0, 4)
if t.bordered {
@@ -1196,6 +1225,12 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
case 's':
flags.preserveSpace = true
skipChars++
case 'n':
flags.number = true
skipChars++
case 'f':
flags.file = true
skipChars++
case 'q':
flags.query = true
default:
@@ -1208,7 +1243,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return false, matchWithoutFlags, flags
}
func hasPreviewFlags(template string) (plus bool, query bool) {
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) {
for _, match := range placeholder.FindAllString(template, -1) {
_, _, flags := parsePlaceholder(match)
if flags.plus {
@@ -1217,11 +1252,32 @@ func hasPreviewFlags(template string) (plus bool, query bool) {
if flags.query {
query = true
}
slot = true
}
return
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
func writeTemporaryFile(data []string, printSep string) string {
f, err := ioutil.TempFile("", "fzf-preview-*")
if err != nil {
errorExit("Unable to create temporary file")
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
activeTempFiles = append(activeTempFiles, f.Name())
return f.Name()
}
func cleanTemporaryFiles() {
for _, filename := range activeTempFiles {
os.Remove(filename)
}
activeTempFiles = []string{}
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
@@ -1251,7 +1307,21 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
if match == "{}" {
for idx, item := range items {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
if flags.number {
n := int(item.text.Index)
if n < 0 {
replacements[idx] = ""
} else {
replacements[idx] = strconv.Itoa(n)
}
} else if flags.file {
replacements[idx] = item.AsString(stripAnsi)
} else {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
}
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ")
}
@@ -1270,7 +1340,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
for idx, item := range items {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := string(joinTokens(trans))
str := joinTokens(trans)
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
@@ -1282,7 +1352,13 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
if !flags.preserveSpace {
str = strings.TrimSpace(str)
}
replacements[idx] = quoteEntry(str)
if !flags.file {
str = quoteEntry(str)
}
replacements[idx] = str
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ")
})
@@ -1299,7 +1375,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
if !valid {
return
}
command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
command := replacePlaceholder(template, t.ansi, t.delimiter, t.printsep, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false)
if !background {
cmd.Stdin = os.Stdin
@@ -1315,6 +1391,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run()
t.tui.Resume(false)
}
cleanTemporaryFiles()
}
func (t *Terminal) hasPreviewer() bool {
@@ -1331,7 +1408,7 @@ func (t *Terminal) hasPreviewWindow() bool {
func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
if t.cy >= 0 && cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item
}
return nil
@@ -1339,7 +1416,7 @@ func (t *Terminal) currentItem() *Item {
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
current := t.currentItem()
plus, query := hasPreviewFlags(template)
_, plus, query := hasPreviewFlags(template)
if !(query && len(t.input) > 0 || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current}
}
@@ -1349,7 +1426,7 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
// 2. or it contains {+} and we have more than one item already selected.
// To do so, we pass an empty Item instead of nil to trigger an update.
if current == nil {
current = &Item{}
current = &minItem
}
var sels []*Item
@@ -1365,9 +1442,18 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
return true, sels
}
func (t *Terminal) selectItem(item *Item) {
func (t *Terminal) selectItem(item *Item) bool {
if len(t.selected) >= t.multi {
return false
}
if _, found := t.selected[item.Index()]; found {
return true
}
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
return true
}
func (t *Terminal) deselectItem(item *Item) {
@@ -1375,12 +1461,12 @@ func (t *Terminal) deselectItem(item *Item) {
t.version++
}
func (t *Terminal) toggleItem(item *Item) {
func (t *Terminal) toggleItem(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selectItem(item)
} else {
t.deselectItem(item)
return t.selectItem(item)
}
t.deselectItem(item)
return true
}
func (t *Terminal) killPreview(code int) {
@@ -1403,7 +1489,7 @@ func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-intChan
t.reqBox.Set(reqQuit, nil)
@@ -1433,7 +1519,6 @@ func (t *Terminal) Loop() {
t.printPrompt()
t.printInfo()
t.printHeader()
t.placeCursor()
t.refresh()
t.mutex.Unlock()
go func() {
@@ -1448,11 +1533,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
if reading {
t.reqBox.Set(reqInfo, nil)
}
}
}()
}
@@ -1473,12 +1557,16 @@ func (t *Terminal) Loop() {
// We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, false, string(t.input), request)
t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request)
cmd := util.ExecCommand(command, true)
if t.pwindow != nil {
env := os.Environ()
env = append(env, fmt.Sprintf("LINES=%d", t.pwindow.Height()))
env = append(env, fmt.Sprintf("COLUMNS=%d", t.pwindow.Width()))
lines := fmt.Sprintf("LINES=%d", t.pwindow.Height())
columns := fmt.Sprintf("COLUMNS=%d", t.pwindow.Width())
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
cmd.Env = env
}
var out bytes.Buffer
@@ -1509,8 +1597,9 @@ func (t *Terminal) Loop() {
cmd.Wait()
finishChan <- true
if out.Len() > 0 || !<-updateChan {
t.reqBox.Set(reqPreviewDisplay, string(out.Bytes()))
t.reqBox.Set(reqPreviewDisplay, out.String())
}
cleanTemporaryFiles()
} else {
t.reqBox.Set(reqPreviewDisplay, "")
}
@@ -1519,9 +1608,6 @@ func (t *Terminal) Loop() {
}
exit := func(getCode func() int) {
if !t.cleanExit && t.fullscreen && t.inlineInfo {
t.placeCursor()
}
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
@@ -1532,8 +1618,8 @@ func (t *Terminal) Loop() {
}
go func() {
var focused *Item
var version int64
var focusedIndex int32 = minItem.Index()
var version int64 = -1
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
@@ -1542,17 +1628,21 @@ func (t *Terminal) Loop() {
switch req {
case reqPrompt:
t.printPrompt()
if t.inlineInfo {
if t.noInfoLine() {
t.printInfo()
}
case reqInfo:
t.printInfo()
case reqList:
t.printList()
currentFocus := t.currentItem()
if currentFocus != focused || version != t.version {
var currentIndex int32 = minItem.Index()
currentItem := t.currentItem()
if currentItem != nil {
currentIndex = currentItem.Index()
}
if focusedIndex != currentIndex || version != t.version {
version = t.version
focused = currentFocus
focusedIndex = currentIndex
if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false)
t.cancelPreview()
@@ -1596,7 +1686,6 @@ func (t *Terminal) Loop() {
exit(func() int { return exitInterrupt })
}
}
t.placeCursor()
t.refresh()
t.mutex.Unlock()
})
@@ -1605,11 +1694,16 @@ func (t *Terminal) Loop() {
looping := true
for looping {
var newCommand *string
changed := false
queryChanged := false
event := t.tui.GetChar()
t.mutex.Lock()
previousInput := t.input
events := []util.EventType{reqPrompt}
previousCx := t.cx
events := []util.EventType{}
req := func(evts ...util.EventType) {
for _, event := range evts {
events = append(events, event)
@@ -1618,16 +1712,23 @@ func (t *Terminal) Loop() {
}
}
}
toggle := func() {
if t.cy < t.merger.Length() {
t.toggleItem(t.merger.Get(t.cy).item)
toggle := func() bool {
if t.cy < t.merger.Length() && t.toggleItem(t.merger.Get(t.cy).item) {
req(reqInfo)
return true
}
return false
}
scrollPreview := func(amount int) {
t.previewer.offset = util.Constrain(
if !t.previewer.more {
return
}
newOffset := util.Constrain(
t.previewer.offset+amount, 0, t.previewer.lines-1)
req(reqPreviewRefresh)
if t.previewer.offset != newOffset {
t.previewer.offset = newOffset
req(reqPreviewRefresh)
}
}
for key, ret := range t.expect {
if keyMatch(key, event) {
@@ -1669,7 +1770,7 @@ func (t *Terminal) Loop() {
t.previewBox.Set(reqPreviewEnqueue, list)
}
}
req(reqList, reqInfo, reqHeader)
req(reqPrompt, reqList, reqInfo, reqHeader)
}
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
@@ -1678,9 +1779,7 @@ func (t *Terminal) Loop() {
}
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
changed = true
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreview(-1)
@@ -1738,27 +1837,43 @@ func (t *Terminal) Loop() {
t.cx--
}
case actSelectAll:
if t.multi {
if t.multi > 0 {
for i := 0; i < t.merger.Length(); i++ {
t.selectItem(t.merger.Get(i).item)
if !t.selectItem(t.merger.Get(i).item) {
break
}
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
t.selected = make(map[int32]selectedItem)
t.version++
if t.multi > 0 {
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
t.deselectItem(t.merger.Get(i).item)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
req(reqList)
}
case actToggleAll:
if t.multi {
if t.multi > 0 {
prevIndexes := make(map[int]struct{})
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
item := t.merger.Get(i).item
if _, found := t.selected[item.Index()]; found {
prevIndexes[i] = struct{}{}
t.deselectItem(item)
}
}
for i := 0; i < t.merger.Length(); i++ {
t.toggleItem(t.merger.Get(i).item)
if _, found := prevIndexes[i]; !found {
item := t.merger.Get(i).item
if !t.selectItem(item) {
break
}
}
}
req(reqList, reqInfo)
}
@@ -1773,14 +1888,12 @@ func (t *Terminal) Loop() {
}
return doAction(action{t: actToggleUp}, mapkey)
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(1, true)
req(reqList)
}
@@ -1798,6 +1911,15 @@ func (t *Terminal) Loop() {
}
case actClearScreen:
req(reqRedraw)
case actClearQuery:
t.input = []rune{}
t.cx = 0
case actClearSelection:
if t.multi > 0 {
t.selected = make(map[int32]selectedItem)
t.version++
req(reqList, reqInfo)
}
case actTop:
t.vset(0)
req(reqList)
@@ -1884,7 +2006,7 @@ func (t *Terminal) Loop() {
if me.S != 0 {
// Scroll
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
if t.multi && me.Mod {
if t.multi > 0 && me.Mod {
toggle()
}
t.vmove(me.S, true)
@@ -1897,7 +2019,7 @@ func (t *Terminal) Loop() {
my -= t.window.Top()
mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
min := 2 + len(t.header)
if t.inlineInfo {
if t.noInfoLine() {
min--
}
h := t.window.Height()
@@ -1924,7 +2046,7 @@ func (t *Terminal) Loop() {
t.cx = mx + t.xoffset
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod {
toggle()
}
req(reqList)
@@ -1935,10 +2057,25 @@ func (t *Terminal) Loop() {
}
}
}
case actReload:
t.failed = nil
valid, list := t.buildPlusList(a.a, false)
if !valid {
// We run the command even when there's no match
// 1. If the template doesn't have any slots
// 2. If the template has {q}
slot, _, query := hasPreviewFlags(a.a)
valid = !slot || query
}
if valid {
command := replacePlaceholder(a.a,
t.ansi, t.delimiter, t.printsep, false, string(t.input), list)
newCommand = &command
}
}
return true
}
changed := false
mapkey := event.Type
if t.jumping == jumpDisabled {
actions := t.keymap[mapkey]
@@ -1952,8 +2089,9 @@ func (t *Terminal) Loop() {
continue
}
t.truncateQuery()
changed = string(previousInput) != string(t.input)
if onChanges, prs := t.keymap[tui.Change]; changed && prs {
queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs {
if !doActions(onChanges, tui.Change) {
continue
}
@@ -1970,16 +2108,24 @@ func (t *Terminal) Loop() {
t.jumping = jumpDisabled
req(reqList)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
if queryChanged {
if t.isPreviewEnabled() {
_, q := hasPreviewFlags(t.preview.command)
_, _, q := hasPreviewFlags(t.preview.command)
if q {
t.version++
}
}
t.eventBox.Set(EvtSearchNew, t.sort)
}
if queryChanged || t.cx != previousCx {
req(reqPrompt)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand})
}
for _, event := range events {
t.reqBox.Set(event, nil)
@@ -2029,7 +2175,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - len(t.header)
if t.inlineInfo {
if t.noInfoLine() {
max++
}
return util.Max(max, 0)

View File

@@ -30,92 +30,92 @@ func TestReplacePlaceholder(t *testing.T) {
t.Errorf("expected: %s, actual: %s", expected, result)
}
}
printsep := "\n"
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
// {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar baz'")
// {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2)
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, false, "query", items1)
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, false, "query", items1)
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo 'bar baz'")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, false, "query", items1)
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, false, "query", items1)
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'")
// Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile("\\w+")
regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, false, "query", items1)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, false, "query", items1)
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ''\\'''")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, false, "query", items1)
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' '")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
// String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter
regex = regexp.MustCompile("[oa]+")
// foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
}

View File

@@ -238,7 +238,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
for _, part := range parts {
output.WriteString(part.ToString())
}
merged = util.ToChars([]byte(output.String()))
merged = util.ToChars(output.Bytes())
}
var prefixLength int32

View File

@@ -73,7 +73,7 @@ func TestTransform(t *testing.T) {
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: " {
if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
@@ -95,7 +95,7 @@ func TestTransform(t *testing.T) {
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||

View File

@@ -39,6 +39,6 @@ func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
return nil
}

View File

@@ -163,9 +163,9 @@ func (r *LightRenderer) findOffset() (row int, col int) {
return -1, -1
}
func repeat(s string, times int) string {
func repeat(r rune, times int) string {
if times > 0 {
return strings.Repeat(s, times)
return strings.Repeat(string(r), times)
}
return ""
}
@@ -345,6 +345,14 @@ func (r *LightRenderer) GetChar() Event {
return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case 28:
return Event{CtrlBackSlash, 0, nil}
case 29:
return Event{CtrlRightBracket, 0, nil}
case 30:
return Event{CtrlCaret, 0, nil}
case 31:
return Event{CtrlSlash, 0, nil}
case ESC:
ev := r.escSequence(&sz)
// Second chance
@@ -518,6 +526,9 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
}
if r.buffer[1] >= '0' && r.buffer[1] <= '9' {
return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil}
}
return Event{Invalid, 0, nil}
}
@@ -547,7 +558,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
r.prevDownTime = now
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
time.Since(r.prevDownTime) < doubleClickDuration {
double = true
}
}
@@ -655,7 +666,7 @@ func (r *LightRenderer) DoesAutoWrap() bool {
return false
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
w := &LightWindow{
renderer: r,
colored: r.theme != nil,
@@ -668,15 +679,20 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
fg: colDefault,
bg: colDefault}
if r.theme != nil {
w.fg = r.theme.Fg
w.bg = r.theme.Bg
if preview {
w.fg = r.theme.PreviewFg
w.bg = r.theme.PreviewBg
} else {
w.fg = r.theme.Fg
w.bg = r.theme.Bg
}
}
w.drawBorder()
return w
}
func (w *LightWindow) drawBorder() {
switch w.border {
switch w.border.shape {
case BorderAround:
w.drawBorderAround()
case BorderHorizontal:
@@ -686,32 +702,30 @@ func (w *LightWindow) drawBorder() {
func (w *LightWindow) drawBorderHorizontal() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
w.CPrint(ColBorder, AttrRegular, repeat(w.border.horizontal, w.width))
}
func (w *LightWindow) drawBorderAround() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
w.CPrint(ColPreviewBorder, AttrRegular,
string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(ColBorder, AttrRegular, "│")
w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
w.CPrint(ColBorder, AttrRegular, "│")
w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
w.CPrint(ColPreviewBorder, AttrRegular, repeat(' ', w.width-2))
w.CPrint(ColPreviewBorder, AttrRegular, string(w.border.vertical))
}
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
w.CPrint(ColPreviewBorder, AttrRegular,
string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
}
func (w *LightWindow) csi(code string) {
w.renderer.csi(code)
}
func (w *LightWindow) stderr(str string) {
w.renderer.stderr(str)
}
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR)
}
@@ -762,7 +776,7 @@ func (w *LightWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
// We should not delete preview window on the right
// csi("K")
w.Print(repeat(" ", w.width-x))
w.Print(repeat(' ', w.width-x))
w.Move(y, x)
}
@@ -858,7 +872,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
width += w
str := string(r)
if r == '\t' {
str = repeat(" ", w)
str = repeat(' ', w)
}
if prefixLength+width <= max {
line += str

View File

@@ -32,6 +32,7 @@ type TcellWindow struct {
left int
width int
height int
normal ColorPair
lastX int
lastY int
moveCursor bool
@@ -61,12 +62,8 @@ func (w *TcellWindow) Refresh() {
}
w.lastX = 0
w.lastY = 0
switch w.borderStyle {
case BorderAround:
w.drawBorder(true)
case BorderHorizontal:
w.drawBorder(false)
}
w.drawBorder()
}
func (w *TcellWindow) FinishFill() {
@@ -288,6 +285,12 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{keyfn('z'), 0, nil}
case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash:
return Event{CtrlBackSlash, 0, nil}
case tcell.KeyCtrlRightSq:
return Event{CtrlRightBracket, 0, nil}
case tcell.KeyCtrlUnderscore:
return Event{CtrlSlash, 0, nil}
case tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
@@ -406,14 +409,18 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
_screen.Show()
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
// TODO
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
normal := ColNormal
if preview {
normal = ColPreview
}
return &TcellWindow{
color: r.theme != nil,
top: top,
left: left,
width: width,
height: height,
normal: normal,
borderStyle: borderStyle}
}
@@ -421,16 +428,16 @@ func (w *TcellWindow) Close() {
// TODO
}
func fill(x, y, w, h int, r rune) {
func fill(x, y, w, h int, n ColorPair, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColNormal.style())
_screen.SetContent(x+lx, y+ly, r, nil, n.style())
}
}
}
func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height, ' ')
fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ')
}
func (w *TcellWindow) Enclose(y int, x int) bool {
@@ -447,13 +454,13 @@ func (w *TcellWindow) Move(y int, x int) {
func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColNormal.style())
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style())
}
w.lastX = x
}
func (w *TcellWindow) Print(text string) {
w.printString(text, ColNormal, 0)
w.printString(text, w.normal, 0)
}
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
@@ -466,7 +473,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
} else {
style = ColNormal.style().
style = w.normal.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
}
@@ -517,7 +524,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
if w.color {
style = pair.style()
} else {
style = ColNormal.style()
style = w.normal.style()
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
@@ -557,20 +564,24 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
}
func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, ColNormal, 0)
return w.fillString(str, w.normal, 0)
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = ColNormal.Fg()
fg = w.normal.Fg()
}
if bg == colDefault {
bg = ColNormal.Bg()
bg = w.normal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg), a)
}
func (w *TcellWindow) drawBorder(around bool) {
func (w *TcellWindow) drawBorder() {
if w.borderStyle.shape == BorderNone {
return
}
left := w.left
right := left + w.width
top := w.top
@@ -578,25 +589,29 @@ func (w *TcellWindow) drawBorder(around bool) {
var style tcell.Style
if w.color {
style = ColBorder.style()
if w.borderStyle.shape == BorderAround {
style = ColPreviewBorder.style()
} else {
style = ColBorder.style()
}
} else {
style = ColNormal.style()
style = w.normal.style()
}
for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
}
if around {
if w.borderStyle.shape == BorderAround {
for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
}
}

View File

@@ -40,6 +40,12 @@ const (
ESC
CtrlSpace
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash
CtrlRightBracket
CtrlCaret
CtrlSlash
Invalid
Resize
Mouse
@@ -117,7 +123,7 @@ func (c Color) is24() bool {
const (
colUndefined Color = -2
colDefault = -1
colDefault Color = -1
)
const (
@@ -164,14 +170,13 @@ func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) is24() bool {
return p.fg.is24() || p.bg.is24()
}
type ColorTheme struct {
Fg Color
Bg Color
PreviewFg Color
PreviewBg Color
DarkBg Color
Gutter Color
Prompt Color
Match Color
Current Color
@@ -200,14 +205,60 @@ type MouseEvent struct {
Mod bool
}
type BorderStyle int
type BorderShape int
const (
BorderNone BorderStyle = iota
BorderNone BorderShape = iota
BorderAround
BorderHorizontal
)
type BorderStyle struct {
shape BorderShape
horizontal rune
vertical rune
topLeft rune
topRight rune
bottomLeft rune
bottomRight rune
}
type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode {
return BorderStyle{
shape: shape,
horizontal: '─',
vertical: '│',
topLeft: '┌',
topRight: '┐',
bottomLeft: '└',
bottomRight: '┘',
}
}
return BorderStyle{
shape: shape,
horizontal: '-',
vertical: '|',
topLeft: '+',
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
}
}
func MakeTransparentBorder() BorderStyle {
return BorderStyle{
shape: BorderAround,
horizontal: ' ',
vertical: ' ',
topLeft: ' ',
topRight: ' ',
bottomLeft: ' ',
bottomRight: ' '}
}
type Renderer interface {
Init()
Pause(clear bool)
@@ -223,7 +274,7 @@ type Renderer interface {
MaxY() int
DoesAutoWrap() bool
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
}
type Window interface {
@@ -272,24 +323,31 @@ var (
Dark256 *ColorTheme
Light256 *ColorTheme
ColNormal ColorPair
ColPrompt ColorPair
ColMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColCursor ColorPair
ColSelected ColorPair
ColHeader ColorPair
ColBorder ColorPair
ColPrompt ColorPair
ColNormal ColorPair
ColMatch ColorPair
ColCursor ColorPair
ColSelected ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentSelected ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColHeader ColorPair
ColBorder ColorPair
ColPreview ColorPair
ColPreviewBorder ColorPair
)
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Fg: colUndefined,
Bg: colUndefined,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: colUndefined,
Gutter: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
@@ -311,7 +369,10 @@ func init() {
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: colBlack,
Gutter: colUndefined,
Prompt: colBlue,
Match: colGreen,
Current: colYellow,
@@ -325,7 +386,10 @@ func init() {
Dark256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: 236,
Gutter: colUndefined,
Prompt: 110,
Match: 108,
Current: 254,
@@ -339,7 +403,10 @@ func init() {
Light256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
PreviewFg: colUndefined,
PreviewBg: colUndefined,
DarkBg: 251,
Gutter: colUndefined,
Prompt: 25,
Match: 66,
Current: 237,
@@ -370,7 +437,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
}
theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
@@ -392,29 +462,37 @@ func initPalette(theme *ColorTheme) {
return ColorPair{fg, bg, idx}
}
if theme != nil {
ColNormal = pair(theme.Fg, theme.Bg)
ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter)
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)
ColCursor = pair(theme.Cursor, theme.DarkBg)
ColSelected = pair(theme.Selected, theme.DarkBg)
ColHeader = pair(theme.Header, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
} else {
ColNormal = pair(colDefault, colDefault)
ColPrompt = pair(colDefault, colDefault)
ColNormal = pair(colDefault, colDefault)
ColMatch = pair(colDefault, colDefault)
ColCurrent = pair(colDefault, colDefault)
ColCurrentMatch = pair(colDefault, colDefault)
ColSpinner = pair(colDefault, colDefault)
ColInfo = pair(colDefault, colDefault)
ColCursor = pair(colDefault, colDefault)
ColSelected = pair(colDefault, colDefault)
ColCurrent = pair(colDefault, colDefault)
ColCurrentMatch = pair(colDefault, colDefault)
ColCurrentCursor = pair(colDefault, colDefault)
ColCurrentSelected = pair(colDefault, colDefault)
ColSpinner = pair(colDefault, colDefault)
ColInfo = pair(colDefault, colDefault)
ColHeader = pair(colDefault, colDefault)
ColBorder = pair(colDefault, colDefault)
ColPreview = pair(colDefault, colDefault)
ColPreviewBorder = pair(colDefault, colDefault)
}
}

View File

@@ -142,6 +142,11 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces
}
func (chars *Chars) TrimTrailingWhitespaces() {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
}
func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
@@ -169,5 +174,13 @@ func (chars *Chars) CopyRunes(dest []rune) {
for idx, b := range chars.slice[:len(dest)] {
dest[idx] = rune(b)
}
return
}
func (chars *Chars) Prepend(prefix string) {
if runes := chars.optionalRunes(); runes != nil {
runes = append([]rune(prefix), runes...)
chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
} else {
chars.slice = append([]byte(prefix), chars.slice...)
}
}

View File

@@ -112,3 +112,13 @@ func DurWithin(
func IsTty() bool {
return isatty.IsTerminal(os.Stdin.Fd())
}
// Once returns a function that returns the specified boolean value only once
func Once(nextResponse bool) func() bool {
state := nextResponse
return func() bool {
prevState := state
state = false
return prevState
}
}

View File

@@ -20,3 +20,21 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3)
}
}
func TestOnce(t *testing.T) {
o := Once(false)
if o() {
t.Error("Expected: false")
}
if o() {
t.Error("Expected: false")
}
o = Once(true)
if !o() {
t.Error("Expected: true")
}
if o() {
t.Error("Expected: false")
}
}

View File

@@ -256,12 +256,12 @@ class TestGoFZF < TestBase
# Testing basic key bindings
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
tmux.until { |lines| lines[-2] == ' 856/100000' }
lines = tmux.capture
assert_equal '> 3910', lines[-4]
assert_equal ' 391', lines[-3]
assert_equal ' 856/100000', lines[-2]
assert_equal '> 391', lines[-1]
tmux.until do |lines|
'> 3910' == lines[-4] &&
' 391' == lines[-3] &&
' 856/100000' == lines[-2] &&
'> 391' == lines[-1]
end
tmux.send_keys :Enter
assert_equal '3910', readonce.chomp
@@ -277,7 +277,7 @@ class TestGoFZF < TestBase
def test_fzf_default_command_failure
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
tmux.until { |lines| lines[-2].include?('FZF_DEFAULT_COMMAND failed') }
tmux.until { |lines| lines[-2].include?('Command failed: false') }
tmux.send_keys :Enter
end
@@ -371,6 +371,65 @@ class TestGoFZF < TestBase
assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_multi_max
tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter
tmux.until { |lines| lines.item_count == 10 }
tmux.send_keys '1'
tmux.until do |lines|
lines[1].include?('[1]/1') && lines[-2].include?('2/10')
end
tmux.send_keys 'A'
tmux.until do |lines|
lines[1].include?('[1 10]/1') && lines[-2].include?('2/10 (2/3)')
end
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
tmux.send_keys 'T'
tmux.until do |lines|
lines[1].include?('[2 3 4]/1') && lines[-2].include?('10/10 (3/3)')
end
%w[T A].each do |key|
tmux.send_keys key
tmux.until do |lines|
lines[1].include?('[1 5 6]/1') && lines[-2].include?('10/10 (3/3)')
end
end
tmux.send_keys :BTab
tmux.until do |lines|
lines[1].include?('[5 6]/2') && lines[-2].include?('10/10 (2/3)')
end
[:BTab, :BTab, 'A'].each do |key|
tmux.send_keys key
tmux.until do |lines|
lines[1].include?('[5 6 2]/3') && lines[-2].include?('10/10 (3/3)')
end
end
tmux.send_keys '2'
tmux.until { |lines| lines[-2].include?('1/10 (3/3)') }
tmux.send_keys 'T'
tmux.until do |lines|
lines[1].include?('[5 6]/2') && lines[-2].include?('1/10 (2/3)')
end
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include?('10/10 (2/3)') }
tmux.send_keys 'A'
tmux.until do |lines|
lines[1].include?('[5 6 1]/1') && lines[-2].include?('10/10 (3/3)')
end
end
def test_with_nth
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
@@ -806,16 +865,22 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '(100)' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(98)' }
tmux.send_keys '100'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys 'C-d'
tmux.until { |lines| lines[-2].include? '(97)' }
tmux.send_keys 'C-u'
tmux.until { |lines| lines.match_count == 100 }
tmux.send_keys 'C-d'
tmux.until { |lines| !lines[-2].include? '(' }
tmux.send_keys :Tab, :Tab
tmux.send_keys :BTab, :BTab
tmux.until { |lines| lines[-2].include? '(2)' }
tmux.send_keys 0
tmux.until { |lines| lines[-2].include? '10/100' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(12)' }
tmux.send_keys :Enter
assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100],
assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100],
readonce.split($INPUT_RECORD_SEPARATOR)
end
@@ -873,7 +938,7 @@ class TestGoFZF < TestBase
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind \\"alt-a:execute(echo [{}] >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"]
opts = %[--bind \\"alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"]
wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } }
writelines tempname, %w[foo'bar foo"bar foo$bar]
tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter
@@ -898,7 +963,7 @@ class TestGoFZF < TestBase
wait['/3']
tmux.send_keys :Enter
readonce
assert_equal %w[[foo'bar] [foo'bar]
assert_equal %w[/foo'bar/ /foo'bar/
/foo"barfoo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/],
File.readlines(output).map(&:chomp)
@@ -1371,7 +1436,7 @@ class TestGoFZF < TestBase
end
def test_preview_hidden
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$LINES-\\$COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-\\$FZF_PREVIEW_LINES-\\$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys '?'
tmux.until { |lines| lines[-2] =~ / {1-1-1-[0-9]+}/ }
@@ -1400,21 +1465,30 @@ class TestGoFZF < TestBase
def test_preview_flags
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}}'), :Enter
tmux.until { |lines| lines[1].include?('{1/1 /1/1 /}') }
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
tmux.until { |lines| lines[1].include?('{1/1 /1/1 //0/0}') }
tmux.send_keys '123'
tmux.until { |lines| lines[1].include?('{////123}') }
tmux.until { |lines| lines[1].include?('{////123//}') }
tmux.send_keys 'C-u', '1'
tmux.until { |lines| lines.match_count == 2 }
tmux.until { |lines| lines[1].include?('{1/1 /1/1 /1}') }
tmux.until { |lines| lines[1].include?('{1/1 /1/1 /1/0/0}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('{10/10 /1/1 /1}') }
tmux.until { |lines| lines[1].include?('{10/10 /1/1 /1/9/0}') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('{10/10 /1 10/1 10 /1}') }
tmux.until { |lines| lines[1].include?('{10/10 /1 10/1 10 /1/9/0 9}') }
tmux.send_keys '2'
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /12}') }
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /12//0 9}') }
tmux.send_keys '3'
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123}') }
tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') }
end
def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar00') }
tmux.send_keys :BTab
tmux.until { |lines| lines[1].include?('foo barbar foobarfoo0101') }
end
def test_preview_q_no_match
@@ -1427,6 +1501,12 @@ class TestGoFZF < TestBase
tmux.until { |lines| !lines[1].include?('foo') }
end
def test_preview_q_no_match_with_initial_query
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}{q}' --query foo), :Enter
tmux.until { |lines| lines.match_count == 0 }
tmux.until { |lines| lines[1].include?('foofoo') }
end
def test_no_clear
tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter
prompt = '> < 10/10'
@@ -1436,6 +1516,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == prompt }
end
def test_info_hidden
tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter
tmux.until { |lines| lines[-2] == '> 1' }
end
def test_change_top
tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter
tmux.until { |lines| lines.match_count == 1000 }
@@ -1513,6 +1598,85 @@ class TestGoFZF < TestBase
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp)
assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count
end
def test_inverse_only_search_should_not_sort_the_result
# Filter
assert_equal(%w[aaaaa b ccc],
`printf '%s\n' aaaaa b ccc BAD | #{FZF} -f '!bad'`.lines.map(&:chomp))
# Interactive
tmux.send_keys(%[printf '%s\n' aaaaa b ccc BAD | #{FZF} -q '!bad'], :Enter)
tmux.until { |lines| lines.item_count == 4 && lines.match_count == 3 }
tmux.until { |lines| lines[-3] == '> aaaaa' }
tmux.until { |lines| lines[-4] == ' b' }
tmux.until { |lines| lines[-5] == ' ccc' }
end
def test_preview_correct_tab_width_after_ansi_reset_code
writelines tempname, ["\x1b[31m+\x1b[m\t\x1b[32mgreen"]
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
tmux.until { |lines| lines[1].include?('+ green') }
end
def test_phony
tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333') }
tmux.send_keys 'foo'
tmux.until { |lines| lines.match_count == 1000 }
tmux.until { |lines| lines[1].include?('1 333foo') }
end
def test_reload
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
tmux.until { |lines| lines.match_count == 998 }
tmux.send_keys 'a'
tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 }
tmux.send_keys 'b'
tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include?('(1/2)') }
tmux.send_keys '555'
tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 }
tmux.until { |lines| !lines[-2].include?('(1/2)') }
end
def test_reload_even_when_theres_no_match
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
tmux.until { |lines| lines.item_count.zero? }
tmux.send_keys :Space
tmux.until { |lines| lines.item_count == 10 }
end
def test_clear_list_when_header_lines_changed_due_to_reload
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| lines.any? { |line| line.include?('9') } }
tmux.send_keys :Space
tmux.until { |lines| lines.none? { |line| line.include?('9') } }
end
def test_clear_query
tmux.send_keys %(: | #{FZF} --query foo --bind space:clear-query), :Enter
tmux.until { |lines| lines.item_count.zero? }
tmux.until { |lines| lines.last.include?('> foo') }
tmux.send_keys 'C-a', 'bar'
tmux.until { |lines| lines.last.include?('> barfoo') }
tmux.send_keys :Space
tmux.until { |lines| lines.last == '>' }
end
def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| lines.match_count == 100 }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include?('(1)') }
tmux.send_keys 'foo'
tmux.until { |lines| lines.match_count.zero? }
tmux.until { |lines| lines[-2].include?('(1)') }
tmux.send_keys :Space
tmux.until { |lines| lines.match_count.zero? }
tmux.until { |lines| !lines[-2].include?('(1)') }
end
end
module TestShell