Compare commits

...

107 Commits

Author SHA1 Message Date
Junegunn Choi
b86838c2b0 0.13.5 2016-08-21 05:02:45 +09:00
Junegunn Choi
1f7d1f9b15 Update Centos Dockerfile to use Go 1.7 2016-08-21 04:54:53 +09:00
Junegunn Choi
f8fdf9618a No need to cache the result in filtering mode (--filter) 2016-08-20 02:06:57 +09:00
Junegunn Choi
827a83efbc Remove Offset slice from Result struct 2016-08-20 01:53:32 +09:00
Junegunn Choi
3e88849386 [vim] Fix "E706: Variable type mismatch for: arg" 2016-08-19 18:02:32 +09:00
Junegunn Choi
608c416207 Add missing sources 2016-08-19 03:27:42 +09:00
Junegunn Choi
62f6ff9d6c [vim] Make arguments to fzf#wrap() optional
fzf#wrap([name string,] [opts dict,] [fullscreen boolean])
2016-08-19 03:05:22 +09:00
Junegunn Choi
37dc273148 Micro-optimizations
- Make structs smaller
- Introduce Result struct and use it to represent matched items instead of
  reusing Item struct for that purpose
- Avoid unnecessary memory allocation
- Avoid growing slice from the initial capacity
- Code cleanup
2016-08-19 02:39:32 +09:00
Junegunn Choi
f7f01d109e Set the upper limit of the number of search go routines 2016-08-19 01:55:38 +09:00
Junegunn Choi
01ee335521 Remove duplicate code 2016-08-18 03:11:54 +09:00
Junegunn Choi
0e0de29b87 Inline function calls in tight loops
By only using leaf functions
2016-08-18 01:48:52 +09:00
Junegunn Choi
babf877fd6 Increase the number of go routines for search
Sort performance increases as the size of each sublist decreases (n in
nlog(n) decreases). Merger is then responsible for merging the sorted
lists in order, and since in most cases we are only interesed in the
matches in the first page on the screen so the overhead in the process
is negligible.
2016-08-18 01:46:05 +09:00
Junegunn Choi
935272824e Setting GOMAXPROCS is no longer needed
https://golang.org/doc/go1.5
2016-08-17 02:21:33 +09:00
Junegunn Choi
3a9532c8fd Increase read buffer size to 64KB 2016-08-16 02:06:15 +09:00
Junegunn Choi
c4c92142a6 0.13.4 2016-08-14 18:10:21 +09:00
Junegunn Choi
d4b6338102 Lint 2016-08-14 17:51:34 +09:00
Junegunn Choi
8df7d962e6 Improve rendering time of long lines 2016-08-14 17:44:11 +09:00
Junegunn Choi
41e916a511 [perf] evaluateBonus can start from sidx - 1 2016-08-14 11:58:47 +09:00
Junegunn Choi
d9c8a9a880 [perf] Remove memory copy when using string delimiter 2016-08-14 04:30:55 +09:00
Junegunn Choi
ddc7bb9064 [perf] Optimize AWK-style tokenizer for --nth
Approx. 50% less memory footprint and 40% improvement in query time
2016-08-14 02:19:29 +09:00
Junegunn Choi
1d4057c209 [perf] Avoid allocating rune array for ascii string
In the best case (all ascii), this reduces the memory footprint by 60%
and the response time by 15% to 20%. In the worst case (every line has
non-ascii characters), 3 to 4% overhead is observed.
2016-08-14 00:41:30 +09:00
Junegunn Choi
822b86942c [test] Clear environment variables 2016-08-13 19:26:36 +09:00
Junegunn Choi
1e74dbb937 :hidden property of previous --preview-window should be cleared
Fix #636. Patch suggested by @edi9999.
2016-08-12 01:16:59 +09:00
Junegunn Choi
7cef92fffe [vim] Delete fzf buffer even when exit status is non-zero
Fix #183
2016-08-02 03:30:17 +09:00
Junegunn Choi
42e4992f06 [vim] Make sure to delete fzf buffer
Close junegunn/fzf.vim#173 and #630
2016-08-02 02:25:02 +09:00
Junegunn Choi
a6066175c6 Merge pull request #630 from kassio/master
Remove `name` option from `termopen`.
2016-07-30 19:05:43 +09:00
Kassio Borges
27444d6b1e Remove name option from termopen.
`termopen` no longer accepts a `name` option, instead we should suffix the
command with `;#NAME`.
2016-07-29 11:10:46 +01:00
Junegunn Choi
d6a99c0391 [vim] v:shell_error can change around redraw!
Patch suggested by Mariusz Atamańczuk
2016-07-28 01:41:11 +09:00
Junegunn Choi
f787f7e651 [vim] Add fzf#wrap helper function
Close #627
2016-07-26 02:37:12 +09:00
Junegunn Choi
a7c9c08371 [vim] Make :FZF command configurable with g:fzf_layout
To make it consistent with the other commands in fzf.vim
2016-07-21 01:47:08 +09:00
Junegunn Choi
fccc93176b 0.13.3 2016-07-16 01:06:53 +09:00
Junegunn Choi
6439a138fe [install] Build fzf if prebuilt binary doesn't work
Close #617
2016-07-16 00:36:35 +09:00
Junegunn Choi
a9a29dff4f Fix duplicate rendering of the last line in preview window 2016-07-15 23:24:14 +09:00
Junegunn Choi
6a52f8b8dd [zsh-completion] setopt localoptions noksh_arrays
Close #607
2016-07-15 01:26:29 +09:00
Junegunn Choi
a1049328d6 [vim] Adjust split size when --header option is set
Close #622
2016-07-14 13:35:18 +09:00
Junegunn Choi
5c2b96bd00 [vim] Fix error with multi-line $FZF_DEFAULT_COMMAND
Close #620
2016-07-13 13:15:14 +09:00
Junegunn Choi
c36413fdf6 [zsh] Suppress error message when pipefail is not supported
Close #615
2016-07-11 17:47:41 +09:00
Junegunn Choi
52cf5af91c [test] Fix test failure on Travis CI
No guarantee in the order in which files are listed
2016-07-10 15:44:44 +09:00
Junegunn Choi
3a4e053af7 [bash] Fall back to send-keys if named paste buffer is not supported
Related: #616
2016-07-10 15:21:28 +09:00
Junegunn Choi
049bc9ec68 [fzf-tmux] Add man page 2016-07-10 14:44:00 +09:00
Junegunn Choi
b461a555b8 [fzf-tmux] Add --version and --help flags 2016-07-10 14:41:06 +09:00
Junegunn Choi
0f87b2d1e1 [fzf-tmux] Use double brackets
For consistency and (negligible) performance improvement
2016-07-10 14:34:29 +09:00
Junegunn Choi
0fb5b76c0d [fzf-tmux] Fail fast if fzf excutable is not found 2016-07-10 14:28:58 +09:00
Junegunn Choi
0c918dd87a Merge pull request #616 from seanlaguna/master
Use tmux buffers for sending output to preserve character encoding
2016-07-10 12:29:32 +09:00
Junegunn Choi
05299a0fee [test] Use tmux buffer in unicode test cases
Related #616
2016-07-10 12:27:01 +09:00
Sean
b36b0a91f5 use tmux buffers for sending output to preserve character encoding 2016-07-09 09:47:20 -05:00
Junegunn Choi
6081eac58a [shell] Suppress alias/function expansion
Close #611
2016-07-07 01:40:14 +09:00
Junegunn Choi
942ba749c7 [vim] Restore working directory even when new window is opened
Close #612
2016-07-06 13:31:04 +09:00
Junegunn Choi
f941012687 Merge pull request #610 from eigengrau/master
[zsh] Re-initialize zle when widgets finish
2016-07-05 21:50:43 +09:00
Sebastian Reuße
fed5e5d5af [zsh] Re-initialize zle when widgets finish
zle automatically calls zle-line-init when it starts to read a new line. Many
Zsh setups use this hook to set the terminal into application mode, since this
will then allow defining keybinds based on the $terminfo variable (the escape
codes in said variable are only valid in application mode).

However, fzf resets the terminal into raw mode, rendering $terminfo values
invalid once the widget has finished. Accordingly, keyboard bindings defined
via $terminfo won’t work anymore.

This fixes the issue by calling zle-line-init when widgets finish. Care is taken
to not call this widget when it is undefined.

Fixes #279
2016-07-05 08:57:11 +02:00
Junegunn Choi
b864885753 [install] Make sure to unset pipefail 2016-07-04 13:05:26 +09:00
Junegunn Choi
64747c2324 [install] Fix error in install script
Close #608
2016-07-04 13:00:30 +09:00
Junegunn Choi
34965edcda [install] Fall back to wget if curl failed
Close #605
2016-07-04 01:41:43 +09:00
Junegunn Choi
bd4377084d Merge pull request #601 from blueyed/zsh-ret-for-fzf-file-widget
zsh: pass through exit code from fzf with fzf-file-widget
2016-06-18 23:17:10 +09:00
Daniel Hahler
38a2076b89 zsh: pass through exit code from widgets
This allows to have a custom widget like the following, which would
additionally accept the line, but only in case of entries being
selected:

    fzf-file-widget-with-accept() {
      zle fzf-file-widget
      if [[ "$?" == 0 ]] && (( $#BUFFER )); then
        zle accept-line
      fi
    }
    zle     -N   fzf-file-widget-with-accept
    bindkey '\e^T' fzf-file-widget-with-accept

With this `<C-a>t` will launch fzf, and simulate the pressing of "Enter"
afterwards.
2016-06-16 20:20:29 +02:00
Junegunn Choi
5759d50d4a 0.13.2 2016-06-16 02:16:13 +09:00
Junegunn Choi
e455836cc9 Fix race condition where preview window is not properly cleared 2016-06-15 13:15:17 +09:00
Junegunn Choi
8a90f26c8a 0.13.1 2016-06-14 21:53:00 +09:00
Junegunn Choi
24e1fabf2e Do not process ANSI codes in --preview output at once
Close #598
2016-06-14 21:52:47 +09:00
Junegunn Choi
c39c039e15 [shell] Add $FZF_CTRL_T_OPTS and $FZF_ALT_C_OPTS
Close #596
2016-06-12 20:48:23 +09:00
Junegunn Choi
07f176f426 Merge pull request #595 from aykamko/speed-up-fzf-completion
Optimize fzf_default_completion binding
2016-06-12 11:56:49 +09:00
Aleks Kamko
19339e3a6d optimize fzf_default_completion binding 2016-06-11 15:19:16 -07:00
Junegunn Choi
3e1d6a7bcf 0.13.0 2016-06-12 02:15:11 +09:00
Junegunn Choi
2bbc12063c Add --preview and --preview-window
Close #587
2016-06-11 19:59:12 +09:00
Junegunn Choi
b8737b724b Ignore controls chars for bracketed paste mode
Close #594
2016-06-11 12:14:34 +09:00
Junegunn Choi
d91c3a2f5e Merge pull request #593 from edi9999/master
Add fzf_prefer_tmux option
2016-06-10 22:58:58 +09:00
Edgar Hipp
fe5db5aadc Add fzf_prefer_tmux option 2016-06-10 09:05:05 +02:00
Junegunn Choi
cf9c957c66 Update test_execute_shell (#590) 2016-06-08 02:16:07 +09:00
Junegunn Choi
68b60c6d19 Update test_execute_multi (#590) 2016-06-08 02:15:22 +09:00
Junegunn Choi
3a644b16a4 Update test_execute (#590) 2016-06-08 02:04:40 +09:00
Junegunn Choi
95b34de339 [bash/zsh] Fix $FZF_CTRL_R_OPTS with option values with spaces 2016-06-08 01:30:26 +09:00
Junegunn Choi
6a431cbf49 [fzf-tmux] Escape $ in arguments
e.g. fzf-tmux -q '$PATH'

Related: #343
2016-06-08 01:27:22 +09:00
Junegunn Choi
56fb2f00b3 Use single-quoted strings in execute action
Close #590
2016-06-08 00:54:21 +09:00
Junegunn Choi
1c86aaf342 [vim/fzf-tmux] Handle fzf project directory with spaces
Close #583
2016-06-03 12:09:31 +09:00
Junegunn Choi
cfc0b18eaa Revert "Change tmux pane title for fzf splits"
This reverts commit f074709fc9.

Close #586. /cc @akashin
2016-06-03 12:02:21 +09:00
Junegunn Choi
412c211655 [vim] Use lcd instead of chdir
https://github.com/junegunn/fzf.vim/issues/147
2016-06-02 22:24:47 +09:00
Junegunn Choi
923feb69ab [zsh] Fix indentation 2016-06-02 22:01:26 +09:00
Junegunn Choi
92dba7035a Merge pull request #584 from jimbocoder/master
Take SSH completion hints from known_hosts
2016-06-02 21:58:59 +09:00
Jim Howell
b8a3ba16a2 [bash/zsh] Take SSH completion hints from known_hosts
Signed-off-by: Junegunn Choi <junegunn.c@gmail.com>
2016-06-02 21:58:01 +09:00
Junegunn Choi
cd5e4d9402 Merge pull request #582 from akashin/master
[fzf-tmux] Change tmux pane title for fzf splits
2016-06-01 17:07:07 +09:00
Andrey Kashin
f074709fc9 Change tmux pane title for fzf splits 2016-06-01 10:19:26 +03:00
Junegunn Choi
e0b29e437b [bash] Use backticks to avoid delay with blink-matching-paren
Close #580
2016-05-29 02:11:50 +09:00
Junegunn Choi
bdb94fba7d [zsh] Fix #579 - Locally unset globsubst 2016-05-26 00:52:06 +09:00
Junegunn Choi
2f364c62f4 0.12.2 2016-05-19 01:55:54 +09:00
Junegunn Choi
7ed9f83662 Validate jump label characters
Also extend default jump labels
2016-05-19 01:46:22 +09:00
Junegunn Choi
f498a9b3fb Revert version number 2016-05-18 22:47:57 +09:00
Junegunn Choi
13330738b8 Do not match jump labels beyond the screen limit 2016-05-18 22:45:34 +09:00
Junegunn Choi
e53535cc61 Update default jump labels 2016-05-18 22:44:31 +09:00
Junegunn Choi
c62fc5e75c More named keys: F5 ~ F10, ALT-/ 2016-05-18 22:25:09 +09:00
Junegunn Choi
70245ad98c [make] Reduce the size of the binaries with -ldflags -w
Related: #555
2016-05-18 13:29:27 +09:00
Junegunn Choi
6d235bceee Add jump and jump-accept actions for --bind
jump and jump-accept implement EasyMotion-like movement in fzf.
Suggested by @mhrebenyuk. Close #569.
2016-05-18 02:10:03 +09:00
Junegunn Choi
4adebfc856 [install] go get -u github.com/junegunn/fzf/src/fzf 2016-05-17 01:41:59 +09:00
Junegunn Choi
faccc0a410 [fzf-tmux] Escape backslash in command-line arguments 2016-05-15 17:07:34 +09:00
Junegunn Choi
9078688baf Add print-query action for --bind
Close #571
2016-05-13 00:51:15 +09:00
Junegunn Choi
9bd8b1d25f Fix typo 2016-05-13 00:44:33 +09:00
Junegunn Choi
dd4be1da38 Allow alt-enter and alt-space for --bind (#571) 2016-05-13 00:43:50 +09:00
Junegunn Choi
66f86e1870 [fzf-tmux] Fix #562 - Check $TMUX instead of $TMUX_PANE 2016-05-11 22:08:14 +09:00
Junegunn Choi
4ab75b68dc Fix flaky test case: test_execute
Should wait until execute action completes
2016-05-11 01:40:49 +09:00
Junegunn Choi
73cb70dbb3 Fix flaky test case: test_file_completion_unicode 2016-05-11 01:25:17 +09:00
Junegunn Choi
d082cccb6d Fix flaky test case: test_ctrl_t_unicode
The width of the pseudo-terminal on Travis CI environment can be small
and cause the line to be wrapped.
2016-05-11 01:18:26 +09:00
Junegunn Choi
88a80e3c2c Determine 256-color capability using tigetnum("colors")
Close #570
2016-05-11 01:07:06 +09:00
Junegunn Choi
24516bcf4d [install] Set a temporary GOPATH 2016-05-09 02:03:08 +09:00
Junegunn Choi
b4c4a642ed Update README
Close #560, #561
2016-05-03 00:07:53 +09:00
Junegunn Choi
0231617857 [neovim] Fix issues with enew and tabnew layouts
Related: #559
2016-04-28 01:25:24 +09:00
Junegunn Choi
7f64fba80f Update Makefile to allow build on i686 (#555) 2016-04-26 01:49:02 +09:00
Junegunn Choi
633aec38f5 Merge pull request #554 from gene-pavlovsky/patch-1
Fix missing reference to UNAME_M
2016-04-25 23:04:29 +09:00
Gene Pavlovsky
d1b402a23c Fix missing reference to UNAME_M
The `Build on $(UNAME_M) is not supported, yet` message was referencing an undefined UNAME_M. Fixed that.
2016-04-24 21:24:10 +03:00
47 changed files with 2750 additions and 1481 deletions

View File

@@ -1,6 +1,54 @@
CHANGELOG CHANGELOG
========= =========
0.13.5
------
- Memory and performance optimization
- Up to 2x performance with half the amount of memory
0.13.4
------
- Performance optimization
- Memory footprint for ascii string is reduced by 60%
- 15 to 20% improvement of query performance
- Up to 45% better performance of `--nth` with non-regex delimiters
- Fixed invalid handling of `hidden` property of `--preview-window`
0.13.3
------
- Fixed duplicate rendering of the last line in preview window
0.13.2
------
- Fixed race condition where preview window is not properly cleared
0.13.1
------
- Fixed UI issue with large `--preview` output with many ANSI codes
0.13.0
------
- Added preview feature
- `--preview CMD`
- `--preview-window POS[:SIZE][:hidden]`
- `{}` in execute action is now replaced to the single-quoted (instead of
double-quoted) string of the current line
- Fixed to ignore control characters for bracketed paste mode
0.12.2
------
- 256-color capability detection does not require `256` in `$TERM`
- Added `print-query` action
- More named keys for binding; <kbd>F1</kbd> ~ <kbd>F10</kbd>,
<kbd>ALT-/</kbd>, <kbd>ALT-space</kbd>, and <kbd>ALT-enter</kbd>
- Added `jump` and `jump-accept` actions that implement [EasyMotion][em]-like
movement
![][jump]
[em]: https://github.com/easymotion/vim-easymotion
[jump]: https://cloud.githubusercontent.com/assets/700826/15367574/b3999dc4-1d64-11e6-85da-28ceeb1a9bc2.png
0.12.1 0.12.1
------ ------

View File

@@ -151,27 +151,6 @@ Many useful examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
own as well. own as well.
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
- Set `FZF_CTRL_T_COMMAND` to override the default command
- `CTRL-R` - Paste the selected command from history onto the command line
- Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory
If you're on a tmux session, fzf will start in a split pane. You may disable
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
`fzf-tmux` script `fzf-tmux` script
----------------- -----------------
@@ -191,6 +170,31 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
It will still work even when you're not on tmux, silently ignoring `-[udlr]` It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts. options, so you can invariably use `fzf-tmux` in your scripts.
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
- 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
- Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort
- Set `FZF_CTRL_R_OPTS` to pass additional options
- `ALT-C` - cd into the selected directory
- Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options
If you're on a tmux session, fzf will start in a split pane. You may disable
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
Fuzzy completion for bash and zsh Fuzzy completion for bash and zsh
--------------------------------- ---------------------------------
@@ -316,10 +320,10 @@ customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])` #### `fzf#run`
For more advanced uses, you can use `fzf#run()` function with the following For more advanced uses, you can use `fzf#run([options])` function with the
options. following options.
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -338,6 +342,17 @@ options.
Examples can be found on [the wiki Examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### `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`, and `g:fzf_history_dir` like `:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
Tips Tips
---- ----

View File

@@ -2,25 +2,51 @@
# fzf-tmux: starts fzf in a tmux pane # fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
fail() {
>&2 echo "$1"
exit 2
}
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
args=() args=()
opt="" opt=""
skip="" skip=""
swap="" swap=""
close="" close=""
term="" term=""
[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines) [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
while [ $# -gt 0 ]; do
help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
Layout
-u [HEIGHT[%]] Split above (up)
-d [HEIGHT[%]] Split below (down)
-l [WIDTH[%]] Split left
-r [WIDTH[%]] Split right
(default: -d 50%)
'
exit
}
while [[ $# -gt 0 ]]; do
arg="$1" arg="$1"
case "$arg" in shift
[[ -z "$skip" ]] && case "$arg" in
-) -)
term=1 term=1
;; ;;
--help)
help
;;
--version)
echo "fzf-tmux (with fzf $("$fzf" --version))"
exit
;;
-w*|-h*|-d*|-u*|-r*|-l*) -w*|-h*|-d*|-u*|-r*|-l*)
if [ -n "$skip" ]; then
args+=("$1")
shift
continue
fi
if [[ "$arg" =~ ^.[lrw] ]]; then if [[ "$arg" =~ ^.[lrw] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
@@ -36,35 +62,33 @@ while [ $# -gt 0 ]; do
close="; tmux swap-pane -D" close="; tmux swap-pane -D"
fi fi
fi fi
if [ ${#arg} -gt 2 ]; then if [[ ${#arg} -gt 2 ]]; then
size="${arg:2}" size="${arg:2}"
else else
shift
if [[ "$1" =~ ^[0-9]+%?$ ]]; then if [[ "$1" =~ ^[0-9]+%?$ ]]; then
size="$1" size="$1"
else
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
shift shift
else
continue continue
fi fi
fi fi
if [[ "$size" =~ %$ ]]; then if [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:((${#size}-1))}
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -p $(( 100 - size ))"
else else
opt="$opt -p $size" opt="$opt -p $size"
fi fi
else else
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) [[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols)
else else
max=$lines max=$lines
fi fi
size=$(( max - size )) size=$(( max - size ))
[ $size -lt 0 ] && size=0 [[ $size -lt 0 ]] && size=0
opt="$opt -l $size" opt="$opt -l $size"
else else
opt="$opt -l $size" opt="$opt -l $size"
@@ -75,16 +99,17 @@ while [ $# -gt 0 ]; do
# "--" can be used to separate fzf-tmux options from fzf options to # "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts # avoid conflicts
skip=1 skip=1
continue
;; ;;
*) *)
args+=("$1") args+=("$arg")
;; ;;
esac esac
shift [[ -n "$skip" ]] && args+=("$arg")
done done
if ! [ -n "$TMUX_PANE" -a $lines -gt 15 ]; then if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then
fzf "${args[@]}" "$fzf" "${args[@]}"
exit $? exit $?
fi fi
@@ -108,7 +133,7 @@ cleanup() {
rm -f $argsf $fifo1 $fifo2 $fifo3 rm -f $argsf $fifo1 $fifo2 $fifo3
# Remove temp window if we were zoomed # Remove temp window if we were zoomed
if [ -n "$zoomed" ]; then if [[ -n "$zoomed" ]]; then
tmux swap-pane -t $original_window \; \ tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \ select-window -t $original_window \; \
kill-window -t $tmp_window \; \ kill-window -t $tmp_window \; \
@@ -117,16 +142,9 @@ cleanup() {
} }
trap cleanup EXIT SIGINT SIGTERM trap cleanup EXIT SIGINT SIGTERM
fail() {
>&2 echo "$1"
exit 2
}
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found"
envs="env TERM=$TERM " envs="env TERM=$TERM "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo2
mkfifo -m o+w $fifo3 mkfifo -m o+w $fifo3
@@ -134,20 +152,22 @@ mkfifo -m o+w $fifo3
# Build arguments to fzf # Build arguments to fzf
opts="" opts=""
for arg in "${args[@]}"; do for arg in "${args[@]}"; do
arg="${arg//\\/\\\\}"
arg="${arg//\"/\\\"}" arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}" arg="${arg//\`/\\\`}"
arg="${arg//$/\\$}"
opts="$opts \"$arg\"" opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "$envs bash $argsf" $swap \ split-window $opt "$envs bash $argsf" $swap \

55
install
View File

@@ -2,13 +2,14 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.12.1 pre=1 || [[ "$@" =~ --pre ]] && version=0.13.5 pre=1 ||
version=0.12.1 pre=0 version=0.13.5 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
binary_arch= binary_arch=
allow_legacy=
help() { help() {
cat << EOF cat << EOF
@@ -37,6 +38,7 @@ for opt in "$@"; do
auto_completion=1 auto_completion=1
key_bindings=1 key_bindings=1
update_config=1 update_config=1
allow_legacy=1
;; ;;
--key-bindings) key_bindings=1 ;; --key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;; --no-key-bindings) key_bindings=0 ;;
@@ -109,6 +111,14 @@ link_fzf_in_path() {
return 1 return 1
} }
try_curl() {
command -v curl > /dev/null && curl -fL $1 | tar -xz
}
try_wget() {
command -v wget > /dev/null && wget -O - $1 | tar -xz
}
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ $pre = 0 ]; then if [ $pre = 0 ]; then
@@ -128,14 +138,13 @@ download() {
fi fi
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
if command -v curl > /dev/null; then set -o pipefail
curl -fL $url | tar -xz if ! (try_curl $url || try_wget $url); then
elif command -v wget > /dev/null; then set +o pipefail
wget -O - $url | tar -xz binary_error="Failed to download with curl and wget"
else
binary_error="curl or wget not found"
return return
fi fi
set +o pipefail
if [ ! -f $1 ]; then if [ ! -f $1 ]; then
binary_error="Failed to download ${1}" binary_error="Failed to download ${1}"
@@ -158,6 +167,9 @@ case "$archi" in
esac esac
install_ruby_fzf() { install_ruby_fzf() {
if [ -z "$allow_legacy" ]; then
ask "Do you want to install legacy Ruby version instead?" && exit 1
fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
# ruby executable # ruby executable
@@ -229,22 +241,25 @@ cd "$fzf_base"
if [ -n "$binary_error" ]; then if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ..." echo "No prebuilt binary for $archi ..."
if command -v go > /dev/null; then else
echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " echo " - $binary_error !!!"
if go get github.com/junegunn/fzf/src/fzf; then fi
echo "OK" if command -v go > /dev/null; then
link_fzf_in_path echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
else if [ -z "${GOPATH-}" ]; then
echo "Failed to build binary ..." export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
install_ruby_fzf mkdir -p "$GOPATH"
fi fi
if go get -u github.com/junegunn/fzf/src/fzf; then
echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
echo "go executable not found. Cannot build binary ..." echo "Failed to build binary ..."
install_ruby_fzf install_ruby_fzf
fi fi
else else
echo " - $binary_error !!!" echo "go executable not found. Cannot build binary ..."
exit 1 install_ruby_fzf
fi fi
fi fi

54
man/man1/fzf-tmux.1 Normal file
View File

@@ -0,0 +1,54 @@
.ig
The MIT License (MIT)
Copyright (c) 2016 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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 "Aug 2016" "fzf 0.13.5" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
.SH SYNOPSIS
.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
.SH DESCRIPTION
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is
designed to work just like fzf except that it does not take up the whole
screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra
options will be silently ignored if you're not on tmux.
.SH OPTIONS
.SS Layout
(default: \fB-d 50%\fR)
.TP
.B "-u [height[%]]"
Split above (up)
.TP
.B "-d [height[%]]"
Split below (down)
.TP
.B "-l [width[%]]"
Split left
.TP
.B "-r [width[%]]"
Split right

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2016" "fzf 0.12.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Aug 2016" "fzf 0.13.5" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -50,10 +50,10 @@ Case-sensitive match
.TP .TP
.BI "-n, --nth=" "N[,..]" .BI "-n, --nth=" "N[,..]"
Comma-separated list of field index expressions for limiting search scope. Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details. See \fBFIELD INDEX EXPRESSION\fR for the details.
.TP .TP
.BI "--with-nth=" "N[,..]" .BI "--with-nth=" "N[,..]"
Transform each item using index expressions within finder Transform the presentation of each line using field index expressions
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
@@ -64,6 +64,7 @@ Do not sort the result
.TP .TP
.B "--tac" .B "--tac"
Reverse the order of the input Reverse the order of the input
.RS .RS
e.g. \fBhistory | fzf --tac --no-sort\fR e.g. \fBhistory | fzf --tac --no-sort\fR
.RE .RE
@@ -73,13 +74,13 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.br .br
.BR length " Prefers item with shorter length" .BR length " Prefers line with shorter length"
.br .br
.BR begin " Prefers item with matched substring closer to the beginning" .BR begin " Prefers line with matched substring closer to the beginning"
.br .br
.BR end " Prefers item with matched substring closer to the end" .BR end " Prefers line with matched substring closer to the end"
.br .br
.BR index " Prefers item that appeared earlier in the input stream" .BR index " Prefers line that appeared earlier in the input stream"
.br .br
.br .br
@@ -90,16 +91,86 @@ Comma-separated list of sort criteria to apply when the scores are tied.
- \fBindex\fR is implicitly appended to the list when not specified - \fBindex\fR is implicitly appended to the list when not specified
.br .br
- Default is \fBlength\fR (or equivalently \fBlength\fR,index) - Default is \fBlength\fR (or equivalently \fBlength\fR,index)
.br
- If \fBend\fR is found in the list, fzf will scan each line backwards
.SS Interface .SS Interface
.TP .TP
.B "-m, --multi" .B "-m, --multi"
Enable multi-select with tab/shift-tab Enable multi-select with tab/shift-tab
.TP .TP
.B "--no-mouse"
Disable mouse
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the
details.
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
.TP
.BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout
.TP
.B "--reverse"
Reverse orientation
.TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.br
.RS
.BR TRBL " Same margin for top, right, bottom, and left"
.br
.BR TB,RL " Vertical, horizontal margin"
.br
.BR T,RL,B " Top, horizontal, bottom margin"
.br
.BR T,R,B,L " Top, right, bottom, left margin"
.br
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.TP
.BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed
in the given order from top to bottom regardless of \fB--reverse\fR option, and
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
\fB--ansi\fR is not set.
.TP
.BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.SS Display
.TP
.B "--ansi" .B "--ansi"
Enable processing of ANSI color codes Enable processing of ANSI color codes
.TP .TP
.B "--no-mouse" .BI "--tabstop=" SPACES
Disable mouse Number of spaces for a tab character (default: 8)
.TP .TP
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
@@ -137,172 +208,7 @@ e.g. \fBfzf --color=bg+:24\fR
.TP .TP
.B "--black" .B "--black"
Use black background Use black background
.TP .SS History
.B "--reverse"
Reverse orientation
.TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.br
.RS
.BR TRBL " Same margin for top, right, bottom, and left"
.br
.BR TB,RL " Vertical, horizontal margin"
.br
.BR T,RL,B " Top, horizontal, bottom margin"
.br
.BR T,R,B,L " Top, right, bottom, left margin"
.br
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.BI "--tabstop=" SPACES
Number of spaces for a tab character (default: 8)
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--hscroll-off=" "COL"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
.TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR.
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. Each key binding expression
follows the following format: \fBKEY:ACTION\fR
.RS
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE
.RS
.B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\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-left\fR
\fIshift-right\fR
\fIdouble-click\fR
or any single character
.RE
.RS
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\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
\fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBselect-all\fR
\fBtoggle\fR
\fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR)
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.RE
.RS
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows.
.RS
\fBfzf --bind "enter:execute(less {})"\fR
.RE
\fB{}\fR is the placeholder for the double-quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
\fBexecute!...!\fR
\fBexecute@...@\fR
\fBexecute#...#\fR
\fBexecute$...$\fR
\fBexecute%...%\fR
\fBexecute^...^\fR
\fBexecute&...&\fR
\fBexecute*...*\fR
\fBexecute;...;\fR
\fBexecute/.../\fR
\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.
.RE
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the double-quoted strings of the
selected entries separated by spaces.
.RE
.TP .TP
.BI "--history=" "HISTORY_FILE" .BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion. Load search history from the specified file and update the file on completion.
@@ -312,17 +218,34 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value. automatically truncated when the number of the lines exceeds the value.
.SS Preview
.TP .TP
.BI "--header=" "STR" .BI "--preview=" "COMMAND"
The given string will be printed as the sticky header. The lines are displayed Execute the given command for the current line and display the result on the
in the given order from top to bottom regardless of \fB--reverse\fR option, and preview window. \fB{}\fR is the placeholder for the quoted string of the
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when current line.
\fB--ansi\fR is not set.
.RS
e.g. \fBfzf --preview="head -$LINES {}"\fR
.RE
.TP .TP
.BI "--header-lines=" "N" .BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
The first N lines of the input are treated as the sticky header. When Determine the layout of the preview window. If the argument ends with
\fB--with-nth\fR is set, the lines are transformed just like the other \fB:hidden\fR, the preview window will be hidden by default until
lines that follow. \fBtoggle-preview\fR action is triggered.
.RS
.B POSITION: (default: right)
\fBup
\fBdown
\fBleft
\fBright
.RE
.RS
e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR
\fBfzf --preview="file {}" --preview-window=down:1\fR
.RE
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -347,6 +270,7 @@ the default enter key. When this option is set, fzf will print the name of the
key pressed as the first line of its output (or as the second line if key pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed \fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key. with the default enter key.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
@@ -354,11 +278,12 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. ncurses finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.SH ENVIRONMENT .SH ENVIRONMENT VARIABLES
.TP .TP
.B FZF_DEFAULT_COMMAND .B FZF_DEFAULT_COMMAND
Default command to use when input is tty Default command to use when input is tty
@@ -412,11 +337,11 @@ occurrences of the string.
.SS Anchored-match .SS Anchored-match
A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
anchored-match term. Then fzf will search for the items that start with or end anchored-match term. Then fzf will search for the lines that start with or end
with the given string. An anchored-match term is also an exact-match term. with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
term from the result. term from the result.
.SS Exact-match by default .SS Exact-match by default
@@ -431,6 +356,119 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
e.g. \fB^core go$ | rb$ | py$\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
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-10]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\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-left\fR
\fIshift-right\fR
\fIdouble-click\fR
or any single character
\fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\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
\fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept)
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit)
\fBselect-all\fR
\fBtoggle\fR
\fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR)
\fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
\fBexecute!...!\fR
\fBexecute@...@\fR
\fBexecute#...#\fR
\fBexecute$...$\fR
\fBexecute%...%\fR
\fBexecute^...^\fR
\fBexecute&...&\fR
\fBexecute*...*\fR
\fBexecute;...;\fR
\fBexecute/.../\fR
\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.
.RE
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the quoted strings of the selected
entries separated by spaces.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -21,7 +21,8 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_height = '40%' let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install' let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0 let s:installed = 0
@@ -49,7 +50,7 @@ function! s:fzf_exec()
throw 'fzf executable not found' throw 'fzf executable not found'
endif endif
endif endif
return s:exec return s:shellesc(s:exec)
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -104,11 +105,114 @@ function! s:warn(msg)
echohl None echohl None
endfunction endfunction
function! s:has_any(dict, keys)
for key in a:keys
if has_key(a:dict, key)
return 1
endif
endfor
return 0
endfunction
function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p')
return
endif
execute a:cmd s:escape(a:target)
endfunction
function! s:common_sink(action, lines) abort
if len(a:lines) < 2
return
endif
let key = remove(a:lines, 0)
let cmd = get(a:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
call s:open(cmd, item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
let expects = map(copy(args), 'type(v:val)')
let tidx = 0
for arg in copy(a:000)
let tidx = index(expects, type(arg), tidx)
if tidx < 0
throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
endif
let args[tidx] = arg
let tidx += 1
unlet arg
endfor
let [name, opts, bang] = args
" Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang
for key in s:layout_keys
if has_key(opts, key)
call remove(opts, key)
endif
endfor
elseif !s:has_any(opts, s:layout_keys)
if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height
else
let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout))
endif
endif
" History: g:fzf_history_dir
let opts.options = get(opts, 'options', '')
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir)
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options])
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
endif
return opts
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
set shell=sh set shell=sh
if has('nvim') && bufexists('term://*:FZF') if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"'))
call s:warn('FZF is already running!') call s:warn('FZF is already running!')
return [] return []
endif endif
@@ -122,7 +226,9 @@ try
endtry endtry
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
let dict.source = $FZF_DEFAULT_COMMAND let temps.source = tempname()
call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source)
let dict.source = (empty($SHELL) ? 'sh' : $SHELL) . ' ' . s:shellesc(temps.source)
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
@@ -135,21 +241,21 @@ try
call writefile(source, temps.input) call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|' let prefix = 'cat '.s:shellesc(temps.input).'|'
else else
throw 'Invalid source type' throw 'invalid source type'
endif endif
else else
let prefix = '' let prefix = ''
endif endif
let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict) let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if has('nvim') if has('nvim') && !tmux
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
call s:popd(dict, ret) call s:callback(dict, lines)
return ret return lines
finally finally
let &shell = oshell let &shell = oshell
endtry endtry
@@ -179,7 +285,7 @@ function! s:fzf_tmux(dict)
endif endif
endfor endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -193,29 +299,24 @@ function! s:pushd(dict)
return 1 return 1
endif endif
let a:dict.prev_dir = cwd let a:dict.prev_dir = cwd
execute 'chdir' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
let a:dict.dir = getcwd() let a:dict.dir = getcwd()
return 1 return 1
endif endif
return 0 return 0
endfunction endfunction
function! s:popd(dict, lines) augroup fzf_popd
" Since anything can be done in the sink function, there is no telling that autocmd!
" the change of the working directory was made by &autochdir setting. autocmd WinEnter * call s:dopopd()
" augroup END
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled. function! s:dopopd()
" FIXME This makes it impossible to change directory from inside the sink if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir()
" function when &autochdir is not used. return
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
if has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
execute 'chdir' s:escape(remove(a:dict, 'prev_dir'))
endif endif
execute 'lcd' s:escape(w:fzf_prev_dir)
unlet w:fzf_prev_dir
endfunction endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
@@ -255,8 +356,9 @@ function! s:execute(dict, command, temps) abort
let command = escaped let command = escaped
endif endif
execute 'silent !'.command execute 'silent !'.command
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:execute_tmux(dict, command, temps) abort function! s:execute_tmux(dict, command, temps) abort
@@ -267,8 +369,9 @@ function! s:execute_tmux(dict, command, temps) abort
endif endif
call system(command) call system(command)
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:calc_size(max, val, dict) function! s:calc_size(max, val, dict)
@@ -285,6 +388,7 @@ function! s:calc_size(max, val, dict)
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
@@ -298,7 +402,7 @@ function! s:split(dict)
\ 'down': ['botright', 'resize', &lines], \ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ppos = s:getpos() let ppos = s:getpos()
try try
for [dir, triple] in items(directions) for [dir, triple] in items(directions)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
@@ -311,7 +415,7 @@ function! s:split(dict)
endif endif
execute cmd sz.'new' execute cmd sz.'new'
execute resz sz execute resz sz
return {} return [ppos, {}]
endif endif
endfor endfor
if s:present(a:dict, 'window') if s:present(a:dict, 'window')
@@ -319,72 +423,104 @@ function! s:split(dict)
else else
execute (tabpagenr()-1).'tabnew' execute (tabpagenr()-1).'tabnew'
endif endif
return { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh } return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
finally finally
setlocal winfixwidth winfixheight setlocal winfixwidth winfixheight
endtry endtry
endfunction endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winopts = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF', 'winopts': winopts } \ 'winopts': winopts, 'command': a:command }
let s:command = a:command function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode
" execute "normal! \<c-^>"
b #
" No other listed buffer
if bufnr('') == self.buf
enew
endif
endif
endfunction
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let pos = s:getpos() if s:getpos() == self.ppos " {'window': 'enew'}
let inplace = pos == s:ppos " {'window': 'enew'}
if inplace
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
endfor endfor
call self.switch_back(1)
else else
if bufnr('') == self.buf if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when " We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted') " there's no other listed buffer (nvim +'set nobuflisted')
close close
endif endif
if pos.tab == s:ppos.tab execute 'tabnext' self.ppos.tab
wincmd p execute self.ppos.win.'wincmd w'
endif
endif endif
if !s:exit_handler(a:code, s:command, 1) if bufexists(self.buf)
execute 'bd!' self.buf
endif
if !s:exit_handler(a:code, self.command, 1)
return return
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
let ret = [] let lines = s:collect(self.temps)
try call s:callback(self.dict, lines)
let ret = s:callback(self.dict, self.temps) call self.switch_back(s:getpos() == self.ppos)
if inplace && bufnr('') == self.buf
execute "normal! \<c-^>"
" No other listed buffer
if bufnr('') == self.buf
bd!
endif
endif
finally
call s:popd(self.dict, ret)
endtry
endfunction endfunction
call s:pushd(a:dict) try
call termopen(a:command, fzf) if s:present(a:dict, 'dir')
call s:popd(a:dict, []) execute 'lcd' s:escape(a:dict.dir)
endif
call termopen(a:command . ';#FZF', fzf)
finally
if s:present(a:dict, 'dir')
lcd -
endif
endtry
setlocal nospell bufhidden=wipe nobuflisted setlocal nospell bufhidden=wipe nobuflisted
setf fzf setf fzf
startinsert startinsert
return [] return []
endfunction endfunction
function! s:callback(dict, temps) abort function! s:collect(temps) abort
let lines = [] try
try return filereadable(a:temps.result) ? readfile(a:temps.result) : []
if filereadable(a:temps.result) finally
let lines = readfile(a:temps.result) for tf in values(a:temps)
silent! call delete(tf)
endfor
endtry
endfunction
function! s:callback(dict, lines) abort
" Since anything can be done in the sink function, there is no telling that
" the change of the working directory was made by &autochdir setting.
"
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled.
" FIXME This makes it impossible to change directory from inside the sink
" function when &autochdir is not used.
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd
let w:fzf_prev_dir = a:dict.prev_dir
endif
try
if has_key(a:dict, 'sink') if has_key(a:dict, 'sink')
for line in lines for line in a:lines
if type(a:dict.sink) == 2 if type(a:dict.sink) == 2
call a:dict.sink(line) call a:dict.sink(line)
else else
@@ -393,76 +529,36 @@ try
endfor endfor
endif endif
if has_key(a:dict, 'sink*') if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines) call a:dict['sink*'](a:lines)
endif endif
endif catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
for tf in values(a:temps) " We may have opened a new window or tab
silent! call delete(tf) if popd
endfor let w:fzf_prev_dir = a:dict.prev_dir
catch call s:dopopd()
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif endif
finally
return lines
endtry
endfunction endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tab split', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
execute cmd s:escape(item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let s:action = get(g:, 'fzf_action', s:default_action) let args = copy(a:000)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {} let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1])) if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif endif
if !a:bang call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
endfunction endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>) command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save let &cpo = s:cpo_save
unlet s:cpo_save unlet s:cpo_save

View File

@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$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 .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -108,7 +108,7 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval "$(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
source "${BASH_SOURCE[0]}" source "${BASH_SOURCE[0]}"
return $ret return $ret
fi fi
@@ -213,15 +213,16 @@ _fzf_complete_kill() {
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | 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 awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -262,8 +263,8 @@ x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.11.3' ]; then if [ "$_fzf_completion_loaded" != '0.11.3' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | eval $(complete | command grep '\-F' | command grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) command grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.11.3 export _fzf_completion_loaded=0.11.3
fi fi

View File

@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$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 .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -31,7 +31,7 @@ fi
########################################################### ###########################################################
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches nnm local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar" # (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
base=${(Q)1} base=${(Q)1}
lbuf=$2 lbuf=$2
@@ -41,10 +41,7 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
if ! setopt | \grep nonomatch > /dev/null; then setopt localoptions nonomatch
nnm=1
setopt nonomatch
fi
dir="$base" dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then if [ -z "$dir" -o -d ${~dir} ]; then
@@ -61,12 +58,12 @@ __fzf_generic_path_completion() {
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
break break
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
dir=${dir%/}/ dir=${dir%/}/
done done
[ -n "$nnm" ] && unsetopt nonomatch
} }
_fzf_path_completion() { _fzf_path_completion() {
@@ -80,7 +77,7 @@ _fzf_dir_completion() {
} }
_fzf_feed_fifo() ( _fzf_feed_fifo() (
rm -f "$1" command rm -f "$1"
mkfifo "$1" mkfifo "$1"
cat <&0 > "$1" & cat <&0 > "$1" &
) )
@@ -101,20 +98,22 @@ _fzf_complete() {
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle redisplay
rm -f "$fifo" typeset -f zle-line-init >/dev/null && zle zle-line-init
command rm -f "$fifo"
} }
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | 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 awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -139,7 +138,7 @@ _fzf_complete_unalias() {
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
setopt localoptions noshwordsplit setopt localoptions noshwordsplit noksh_arrays
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -164,6 +163,7 @@ fzf-completion() {
LBUFFER="$LBUFFER$matches" LBUFFER="$LBUFFER$matches"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
# Trigger sequence given # Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
@@ -184,8 +184,11 @@ fzf-completion() {
fi fi
} }
[ -z "$fzf_default_completion" ] && [ -z "$fzf_default_completion" ] && {
fzf_default_completion=$(bindkey '^I' | \grep -v undefined-key | awk '{print $2}') binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(w)2]
unset binding
}
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion

View File

@@ -5,7 +5,7 @@ __fzf_select__() {
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | fzf -m | while read -r item; do eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -26,7 +26,7 @@ __fzf_select_tmux__() {
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
} }
fzf-file-widget() { fzf-file-widget() {
@@ -43,7 +43,7 @@ __fzf_cd__() {
local cmd dir local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
dir=$(eval "$cmd" | $(__fzfcmd) +m) && printf 'cd %q' "$dir" dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir"
} }
__fzf_history__() ( __fzf_history__() (
@@ -51,8 +51,8 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
\grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
else else
@@ -67,7 +67,7 @@ __fzf_use_tmux__() {
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 [ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 __fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then if [[ ! -o vi ]]; then
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind '"\er": redraw-current-line' bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line' bind '"\e^": history-expand-line'
@@ -82,10 +82,10 @@ if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"' bind '"\C-r": " \C-e\C-u`__fzf_history__`\e\C-e\e^\er"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"'
else else
# We'd usually use "\e" to enter vi-movement-mode so we can do our magic, # We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
# but this incurs a very noticeable delay of a half second or so, # but this incurs a very noticeable delay of a half second or so,

View File

@@ -19,7 +19,7 @@ function fzf_key_bindings
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result" eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS > $TMPDIR/fzf.result"
and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
@@ -37,7 +37,7 @@ function fzf_key_bindings
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"
# Fish hangs if the command before pipe redirects (2> /dev/null) # Fish hangs if the command before pipe redirects (2> /dev/null)
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m > $TMPDIR/fzf.result" eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result"
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result) and cd (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint

View File

@@ -8,10 +8,13 @@ __fsel() {
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | $(__fzfcmd) -m | while read item; do setopt localoptions pipefail 2> /dev/null
eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$?
echo echo
return $ret
} }
__fzfcmd() { __fzfcmd() {
@@ -20,7 +23,10 @@ __fzfcmd() {
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
local ret=$?
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
@@ -29,8 +35,12 @@ bindkey '^T' fzf-file-widget
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
cd "${$(eval "$cmd" | $(__fzfcmd) +m):-.}" setopt localoptions pipefail 2> /dev/null
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"
local ret=$?
zle reset-prompt zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
bindkey '\ec' fzf-cd-widget bindkey '\ec' fzf-cd-widget
@@ -38,7 +48,9 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") ) setopt localoptions noglobsubst pipefail 2> /dev/null
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
@@ -46,6 +58,8 @@ fzf-history-widget() {
fi fi
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey '^R' fzf-history-widget

View File

@@ -11,18 +11,18 @@ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
# Install Go 1.5 # Install Go 1.7
RUN cd / && curl \ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \
tar -xz && mv go go1.5 tar -xz && mv go go1.7
# Install RPMs for building static 32-bit binary # Install RPMs for building static 32-bit binary
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \ RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
ENV GOROOT_BOOTSTRAP /go1.4 ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.5 ENV GOROOT /go1.7
ENV PATH /go1.5/bin:$PATH ENV PATH /go1.7/bin:$PATH
# For i386 build # For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash RUN cd $GOROOT/src && GOARCH=386 ./make.bash

View File

@@ -7,10 +7,6 @@ else ifeq ($(UNAME_S),Linux)
endif endif
endif endif
ifneq ($(shell uname -m),x86_64)
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BINDIR := $(shell dirname $(ROOTDIR))/bin BINDIR := $(shell dirname $(ROOTDIR))/bin
@@ -26,15 +22,22 @@ RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
export GOPATH export GOPATH
all: release UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
release: test build all: fzf/$(BINARY)
release: test fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64) rm -f $(RELEASE32) $(RELEASE64)
build: fzf/$(BINARY32) fzf/$(BINARY64)
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
@@ -44,7 +47,7 @@ deps: $(SRCDIR) $(SOURCES)
android-build: $(SRCDIR) android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7)
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
@@ -54,20 +57,20 @@ test: deps
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
uninstall: uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY)
clean: clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -tags "$(TAGS)" -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps fzf/$(BINARY64): deps
cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR)
cd $(BINDIR) && ln -sf $(BINARY64) fzf cd $(BINDIR) && ln -sf $(BINARY) fzf
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
@@ -98,7 +101,7 @@ centos: docker-centos
linux: docker-centos linux: docker-centos
docker run $(DOCKEROPTS) junegunn/centos-sandbox \ docker run $(DOCKEROPTS) junegunn/centos-sandbox \
/bin/bash -ci 'cd /fzf/src; make TAGS=static' /bin/bash -ci 'cd /fzf/src; make TAGS=static release'
ubuntu-android: docker-android ubuntu-android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
@@ -108,6 +111,6 @@ android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
/bin/bash -ci 'cd /fzf/src; GOOS=android make android-build' /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
.PHONY: all build deps release test install uninstall clean \ .PHONY: all deps release test install uninstall clean \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android android-build docker-android ubuntu-android android

View File

@@ -79,7 +79,7 @@ Build
```sh ```sh
# Build fzf executables and tarballs # Build fzf executables and tarballs
make make release
# Install the executable to ../bin directory # Install the executable to ../bin directory
make install make install

View File

@@ -15,21 +15,21 @@ import (
* In short: They try to do as little work as possible. * In short: They try to do as little work as possible.
*/ */
func runeAt(runes []rune, index int, max int, forward bool) rune { func indexAt(index int, max int, forward bool) int {
if forward { if forward {
return runes[index] return index
} }
return runes[max-index-1] return max - index - 1
} }
// Result conatins the results of running a match function. // Result conatins the results of running a match function.
type Result struct { type Result struct {
Start int32 Start int
End int32 End int
// Items are basically sorted by the lengths of matched substrings. // Items are basically sorted by the lengths of matched substrings.
// But we slightly adjust the score with bonus for better results. // But we slightly adjust the score with bonus for better results.
Bonus int32 Bonus int
} }
type charClass int type charClass int
@@ -42,14 +42,14 @@ const (
charNumber charNumber
) )
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int {
var bonus int32 var bonus int
pidx := 0 pidx := 0
lenPattern := len(pattern) lenPattern := len(pattern)
consecutive := false consecutive := false
prevClass := charNonWord prevClass := charNonWord
for index := 0; index < eidx; index++ { for index := util.Max(0, sidx-1); index < eidx; index++ {
char := runes[index] char := text.Get(index)
var class charClass var class charClass
if unicode.IsLower(char) { if unicode.IsLower(char) {
class = charLower class = charLower
@@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
class = charNonWord class = charNonWord
} }
var point int32 var point int
if prevClass == charNonWord && class != charNonWord { if prevClass == charNonWord && class != charNonWord {
// Word boundary // Word boundary
point = 2 point = 2
@@ -107,7 +107,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
} }
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}
} }
@@ -125,11 +125,11 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
sidx := -1 sidx := -1
eidx := -1 eidx := -1
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
for index := range runes { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) char := text.Get(indexAt(index, lenRunes, forward))
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string
if !caseSensitive { if !caseSensitive {
@@ -142,7 +142,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
@@ -157,7 +157,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
if sidx >= 0 && eidx >= 0 { if sidx >= 0 && eidx >= 0 {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := runeAt(runes, index, lenRunes, forward) char := text.Get(indexAt(index, lenRunes, forward))
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -166,7 +166,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
@@ -181,8 +181,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
sidx, eidx = lenRunes-eidx, lenRunes-sidx sidx, eidx = lenRunes-eidx, lenRunes-sidx
} }
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
@@ -194,12 +194,12 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
// //
// We might try to implement better algorithms in the future: // We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm // http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}
} }
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
if lenRunes < lenPattern { if lenRunes < lenPattern {
@@ -208,7 +208,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
pidx := 0 pidx := 0
for index := 0; index < lenRunes; index++ { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) char := text.Get(indexAt(index, lenRunes, forward))
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -216,7 +216,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if pchar == char { if pchar == char {
pidx++ pidx++
if pidx == lenPattern { if pidx == lenPattern {
@@ -228,8 +228,8 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
sidx = lenRunes - (index + 1) sidx = lenRunes - (index + 1)
eidx = lenRunes - (index - lenPattern + 1) eidx = lenRunes - (index - lenPattern + 1)
} }
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
} else { } else {
index -= pidx index -= pidx
@@ -240,13 +240,13 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(runes) < len(pattern) { if text.Length() < len(pattern) {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index] char := text.Get(index)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -255,21 +255,20 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
return Result{0, int32(lenPattern), return Result{0, lenPattern,
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)}
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
runes := util.TrimRight(input) trimmedLen := text.Length() - text.TrailingWhitespaces()
trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
if diff < 0 { if diff < 0 {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index+diff] char := text.Get(index + diff)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -280,22 +279,22 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune)
lenPattern := len(pattern) lenPattern := len(pattern)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen eidx := trimmedLen
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
// Note: EqualMatch always return a zero bonus. // Note: EqualMatch always return a zero bonus.
if len(runes) != len(pattern) { if text.Length() != len(pattern) {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
runesStr := string(runes) runesStr := text.ToString()
if !caseSensitive { if !caseSensitive {
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }
if runesStr == string(pattern) { if runesStr == string(pattern) {
return Result{0, int32(len(pattern)), 0} return Result{0, len(pattern), 0}
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }

View File

@@ -3,13 +3,15 @@ package algo
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, bonus int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) res := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern))
if res.Start != sidx { if res.Start != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern)
} }

View File

@@ -36,7 +36,7 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
} }
func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
@@ -46,7 +46,11 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat
idx := 0 idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) { for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
output.WriteString(str[idx:offset[0]]) prev := str[idx:offset[0]]
output.WriteString(prev)
if proc != nil && !proc(prev, state) {
return "", nil, nil
}
newState := interpretCode(str[offset[0]:offset[1]], state) newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) { if !newState.equals(state) {
@@ -77,7 +81,13 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
} }
} }
return output.String(), offsets, state if proc != nil {
proc(rest, state)
}
if len(offsets) == 0 {
return output.String(), nil, state
}
return output.String(), &offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -16,8 +16,8 @@ func TestExtractColor(t *testing.T) {
src := "hello world" src := "hello world"
var state *ansiState var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
output, ansiOffsets, newState := extractColor(src, state) output, ansiOffsets, newState := extractColor(src, state, nil)
state = newState state = newState
if output != "hello world" { if output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: {}", output)
@@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) {
assertion(ansiOffsets, state) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert((*offsets)[0], 0, 11, -1, -1, true)
}) })
state = nil state = nil
src = "\x1b[1mhello \x1b[mworld" src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, -1, -1, true) assert((*offsets)[0], 0, 6, -1, -1, true)
}) })
state = nil state = nil
src = "\x1b[1mhello \x1b[Kworld" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert((*offsets)[0], 0, 11, -1, -1, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mworld" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert((*offsets)[0], 6, 11, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert((*offsets)[0], 6, 11, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 4, 5, true) assert((*offsets)[0], 6, 9, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(*offsets) != 3 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 8, 4, 233, true) assert((*offsets)[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true) assert((*offsets)[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false) assert((*offsets)[2], 10, 11, 161, -1, false)
}) })
// {38,48};5;{38,48} // {38,48};5;{38,48}
state = nil state = nil
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 38, 48, true) assert((*offsets)[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true) assert((*offsets)[1], 9, 10, 48, 38, true)
}) })
src = "hello \x1b[32;1mworld" src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 2, -1, true) assert((*offsets)[0], 6, 11, 2, -1, true)
}) })
src = "hello world" src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, 2, -1, true) assert((*offsets)[0], 0, 11, 2, -1, true)
}) })
src = "hello \x1b[0;38;5;200;48;5;100mworld" src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
if state.fg != 200 || state.bg != 100 || state.bold { if state.fg != 200 || state.bg != 100 || state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, 2, -1, true) assert((*offsets)[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false) assert((*offsets)[1], 6, 11, 200, 100, false)
}) })
} }

View File

@@ -3,7 +3,7 @@ package fzf
import "sync" import "sync"
// queryCache associates strings to lists of items // queryCache associates strings to lists of items
type queryCache map[string][]*Item type queryCache map[string][]*Result
// ChunkCache associates Chunk and query string to lists of items // ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct { type ChunkCache struct {
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
} }
// Add adds the list to the cache // Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return return
} }
@@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
} }
// Find is called to lookup ChunkCache // Find is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
if len(key) == 0 || !chunk.IsFull() { if len(key) == 0 || !chunk.IsFull() {
return nil, false return nil, false
} }

View File

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

View File

@@ -3,6 +3,8 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
@@ -10,7 +12,7 @@ func TestChunkList(t *testing.T) {
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byMatchLen, byLength}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} return &Item{text: util.ToChars(s), index: int32(i * 2)}
}) })
// Snapshot // Snapshot
@@ -39,11 +41,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 { if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
last := func(arr [5]int32) int32 { if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
return arr[len(arr)-1] (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
}
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 ||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {

View File

@@ -8,14 +8,15 @@ import (
const ( const (
// Current version // Current version
version = "0.12.1" version = "0.13.5"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader // Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
readerBufferSize = 64 * 1024
// Terminal // Terminal
initialDelay = 20 * time.Millisecond initialDelay = 20 * time.Millisecond
@@ -36,6 +37,9 @@ const (
// History // History
defaultHistoryMax int = 1000 defaultHistoryMax int = 1000
// Jump labels
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
) )
// fzf events // fzf events

View File

@@ -28,16 +28,11 @@ package fzf
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
/* /*
Reader -> EvtReadFin Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart) Reader -> EvtReadNew -> Matcher (restart)
@@ -49,8 +44,6 @@ Matcher -> EvtHeader -> Terminal (update header)
// Run starts fzf // Run starts fzf
func Run(opts *Options) { func Run(opts *Options) {
initProcs()
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
@@ -63,29 +56,29 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.BytesToRunes(data), nil return util.ToChars(data), nil
} }
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) { ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
return data, nil return util.RunesToChars(data), nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return []rune(trimmed), offsets return util.RunesToChars([]rune(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return []rune(trimmed), nil return util.RunesToChars([]rune(trimmed)), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) { ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
return ansiProcessor([]byte(string(data))) return ansiProcessor([]byte(string(data)))
} }
} }
@@ -100,29 +93,28 @@ func Run(opts *Options) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
runes, colors := ansiProcessor(data) chars, colors := ansiProcessor(data)
return &Item{ return &Item{
text: runes, index: int32(index),
colors: colors, text: chars,
rank: buildEmptyRank(int32(index))} colors: colors}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
runes := util.BytesToRunes(data) tokens := Tokenize(util.ToChars(data), opts.Delimiter)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans))) header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
textRunes := joinTokens(trans)
item := Item{ item := Item{
text: joinTokens(trans), index: int32(index),
origText: &runes, origText: &data,
colors: nil, colors: nil}
rank: buildEmptyRank(int32(index))}
trimmed, colors := ansiProcessorRunes(item.text) trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@@ -151,7 +143,7 @@ func Run(opts *Options) {
} }
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Filter == nil,
opts.Nth, opts.Delimiter, runes) opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -169,9 +161,11 @@ func Run(opts *Options) {
reader := Reader{ reader := Reader{
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil {
fmt.Println(string(item.text)) if result, _ := pattern.MatchItem(item); result != nil {
found = true fmt.Println(item.text.ToString())
found = true
}
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
@@ -185,7 +179,7 @@ func Run(opts *Options) {
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString(opts.Ansi)) fmt.Println(merger.Get(i).item.AsString(opts.Ansi))
found = true found = true
} }
} }
@@ -265,7 +259,7 @@ func Run(opts *Options) {
fmt.Println() fmt.Println()
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString(opts.Ansi)) fmt.Println(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) os.Exit(exitOk)

View File

@@ -80,7 +80,16 @@ const (
F2 F2
F3 F3
F4 F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS AltBS
AltA AltA
AltB AltB
@@ -104,11 +113,14 @@ const (
ColCursor ColCursor
ColSelected ColSelected
ColHeader ColHeader
ColUser ColBorder
ColUser // Should be the last entry
) )
const ( const (
doubleClickDuration = 500 * time.Millisecond doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
) )
type ColorTheme struct { type ColorTheme struct {
@@ -125,6 +137,7 @@ type ColorTheme struct {
Cursor int16 Cursor int16
Selected int16 Selected int16
Header int16 Header int16
Border int16
} }
type Event struct { type Event struct {
@@ -159,6 +172,49 @@ var (
DarkBG int DarkBG int
) )
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border {
attr := _color(ColBorder, false)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func EmptyTheme() *ColorTheme {
return &ColorTheme{
UseDefault: true,
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func init() { func init() {
_prevDownTime = time.Unix(0, 0) _prevDownTime = time.Unix(0, 0)
_clickY = []int{} _clickY = []int{}
@@ -176,7 +232,8 @@ func init() {
Info: C.COLOR_WHITE, Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED, Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA, Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN} Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -190,7 +247,8 @@ func init() {
Info: 144, Info: 144,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 109} Header: 109,
Border: 59}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -204,7 +262,8 @@ func init() {
Info: 101, Info: 101,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 31} Header: 31,
Border: 145}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -280,44 +339,59 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
if theme != nil { if theme != nil {
C.start_color() C.start_color()
initPairs(theme, black) var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
baseTheme = Dark256
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored _color = attrColored
} else { } else {
_color = attrMono _color = attrMono
} }
} }
func initPairs(theme *ColorTheme, black bool) { func override(a int16, b int16) C.short {
fg := C.short(theme.Fg) if b == colUndefined {
bg := C.short(theme.Bg) return C.short(a)
}
return C.short(b)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
fg := override(baseTheme.Fg, theme.Fg)
bg := override(baseTheme.Bg, theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else if theme.UseDefault { } else if theme.UseDefault {
fg = -1 fg = colDefault
bg = -1 bg = colDefault
C.use_default_colors() C.use_default_colors()
} }
if theme.UseDefault { if theme.UseDefault {
FG = -1 FG = colDefault
BG = -1 BG = colDefault
} else { } else {
FG = int(fg) FG = int(fg)
BG = int(bg) BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg)) C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
} }
CurrentFG = int(theme.Current) currentFG := override(baseTheme.Current, theme.Current)
DarkBG = int(theme.DarkBg) darkBG := override(baseTheme.DarkBg, theme.DarkBg)
darkBG := C.short(DarkBG) CurrentFG = int(currentFG)
C.init_pair(ColPrompt, C.short(theme.Prompt), bg) DarkBG = int(darkBG)
C.init_pair(ColMatch, C.short(theme.Match), bg) C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColCurrent, C.short(theme.Current), darkBG) C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG) C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColSpinner, C.short(theme.Spinner), bg) C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColInfo, C.short(theme.Info), bg) C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG) C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColHeader, C.short(theme.Header), bg) C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
} }
func Close() { func Close() {
@@ -373,7 +447,9 @@ func mouseSequence(sz *int) Event {
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100 mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2 s := 1 - int(_buf[3]%2)*2
return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}} x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -384,6 +460,12 @@ func escSequence(sz *int) Event {
} }
*sz = 2 *sz = 2
switch _buf[1] { switch _buf[1] {
case 13:
return Event{AltEnter, 0, nil}
case 32:
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil}
case 98: case 98:
return Event{AltB, 0, nil} return Event{AltB, 0, nil}
case 100: case 100:
@@ -429,6 +511,20 @@ func escSequence(sz *int) Event {
*sz = 4 *sz = 4
switch _buf[2] { switch _buf[2] {
case 50: case 50:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case 51: case 51:
return Event{Del, 0, nil} return Event{Del, 0, nil}
@@ -442,6 +538,21 @@ func escSequence(sz *int) Event {
switch _buf[3] { switch _buf[3] {
case 126: case 126:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59: case 59:
if len(_buf) != 6 { if len(_buf) != 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -511,17 +622,25 @@ func GetChar() Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
func Move(y int, x int) { func (w *Window) Close() {
C.move(C.int(y), C.int(x)) C.delwin(w.win)
} }
func MoveAndClear(y int, x int) { func (w *Window) Enclose(y int, x int) bool {
Move(y, x) return bool(C.wenclose(w.win, C.int(y), C.int(x)))
C.clrtoeol()
} }
func Print(text string) { func (w *Window) Move(y int, x int) {
C.addstr(C.CString(strings.Map(func(r rune) rune { C.wmove(w.win, C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win)
}
func (w *Window) Print(text string) {
C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune {
if r < 32 { if r < 32 {
return -1 return -1
} }
@@ -529,11 +648,11 @@ func Print(text string) {
}, text))) }, text)))
} }
func CPrint(pair int, bold bool, text string) { func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold) attr := _color(pair, bold)
C.attron(attr) C.wattron(w.win, attr)
Print(text) w.Print(text)
C.attroff(attr) C.wattroff(w.win, attr)
} }
func Clear() { func Clear() {
@@ -548,6 +667,30 @@ func Refresh() {
C.refresh() C.refresh()
} }
func (w *Window) Erase() {
C.werase(w.win)
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool {
attr := _color(PairFor(fg, bg), bold)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)
return ret
}
func (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
func PairFor(fg int, bg int) int { func PairFor(fg int, bg int) int {
key := (fg << 8) + bg key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs { if found, prs := _colorMap[key]; prs {

View File

@@ -1,291 +1,39 @@
package fzf package fzf
import ( import (
"math" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/fzf/src/curses"
) )
// Offset holds three 32-bit integers denoting the offsets of a matched substring
type Offset [3]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text []rune index int32
origText *[]rune text util.Chars
origText *[]byte
colors *[]ansiOffset
transformed []Token transformed []Token
offsets []Offset
colors []ansiOffset
rank [5]int32
bonus int32
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
func isRankValid(rank [5]int32) bool {
// Exclude ordinal index
for _, r := range rank[:4] {
if r > 0 {
return true
}
}
return false
}
func buildEmptyRank(index int32) [5]int32 {
return [5]int32{0, 0, 0, 0, index}
} }
// Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.rank[4] return item.index
} }
// Rank calculates rank of the Item // Colors returns ansiOffsets of the Item
func (item *Item) Rank(cache bool) [5]int32 { func (item *Item) Colors() []ansiOffset {
if cache && isRankValid(item.rank) { if item.colors == nil {
return item.rank return []ansiOffset{}
} }
matchlen := 0 return *item.colors
prevEnd := 0
lenSum := 0
minBegin := math.MaxInt32
for _, offset := range item.offsets {
begin := int(offset[0])
end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := buildEmptyRank(item.Index())
for idx, criterion := range sortCriteria {
var val int32
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxInt32
} else {
// It is extremely unlikely that bonus exceeds 128
val = 128*int32(matchlen) - item.bonus
}
case byLength:
// It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
// If offsets is empty, lenSum will be 0, but we don't care
val = int32(lenSum)
} else {
val = int32(len(item.text))
}
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
for idx, r := range item.text {
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = int32(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = int32(1 + len(item.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
rank[idx] = val
}
if cache {
item.rank = rank
}
return rank
} }
// AsString returns the original string // AsString returns the original string
func (item *Item) AsString(stripAnsi bool) string { func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr(stripAnsi)
}
// StringPtr returns the pointer to the original string
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil { if item.origText != nil {
if stripAnsi { if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil) trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
return &trimmed return trimmed
} }
orig := string(*item.origText) return string(*item.origText)
return &orig
} }
str := string(item.text) return item.text.ToString()
return &str
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Item
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank[idx]
right := jrank[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank[4] <= jrank[4]) != tac
} }

View File

@@ -1,108 +1,23 @@
package fzf package fzf
import ( import (
"math"
"sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util"
) )
func TestOffsetSort(t *testing.T) { func TestStringPtr(t *testing.T) {
offsets := []Offset{ orig := []byte("\x1b[34mfoo")
Offset{3, 5}, Offset{2, 7}, text := []byte("\x1b[34mbar")
Offset{1, 3}, Offset{2, 9}} item := Item{origText: &orig, text: util.ToChars(text)}
sort.Sort(ByOrder(offsets)) if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
t.Fail()
if offsets[0][0] != 1 || offsets[0][1] != 3 || }
offsets[1][0] != 2 || offsets[1][1] != 7 || if item.AsString(true) != "foo" {
offsets[2][0] != 2 || offsets[2][1] != 9 || t.Fail()
offsets[3][0] != 3 || offsets[3][1] != 5 { }
t.Error("Invalid order:", offsets) item.origText = nil
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
t.Fail()
} }
} }
func TestRankComparison(t *testing.T) {
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestItemRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}}
rank1 := item1.Rank(true)
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
item2 := Item{text: strs[0], offsets: []Offset{}}
items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 {
t.Error(items)
}
items = []*Item{&item2, &item1, &item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item2 ||
items[2] != &item1 || items[3] != &item1 {
t.Error(items)
}
// Sort by relevance
item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items))
if items[0] != &item6 || items[1] != &item4 ||
items[2] != &item5 || items[3] != &item3 ||
items[4] != &item2 || items[5] != &item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -43,7 +43,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
tac: tac, tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: util.Min(8*runtime.NumCPU(), 32),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger)}
} }
@@ -106,18 +106,19 @@ func (m *Matcher) Loop() {
} }
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
perSlice := len(chunks) / m.partitions partitions := m.partitions
perSlice := len(chunks) / partitions
// No need to parallelize
if perSlice == 0 { if perSlice == 0 {
return [][]*Chunk{chunks} partitions = len(chunks)
perSlice = 1
} }
slices := make([][]*Chunk, m.partitions) slices := make([][]*Chunk, partitions)
for i := 0; i < m.partitions; i++ { for i := 0; i < partitions; i++ {
start := i * perSlice start := i * perSlice
end := start + perSlice end := start + perSlice
if i == m.partitions-1 { if i == partitions-1 {
end = len(chunks) end = len(chunks)
} }
slices[i] = chunks[start:end] slices[i] = chunks[start:end]
@@ -127,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct { type partialResult struct {
index int index int
matches []*Item matches []*Result
} }
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@@ -154,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
waitGroup.Add(1) waitGroup.Add(1)
go func(idx int, chunks []*Chunk) { go func(idx int, chunks []*Chunk) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
sliceMatches := []*Item{} count := 0
for _, chunk := range chunks { allMatches := make([][]*Result, len(chunks))
for idx, chunk := range chunks {
matches := request.pattern.Match(chunk) matches := request.pattern.Match(chunk)
sliceMatches = append(sliceMatches, matches...) allMatches[idx] = matches
count += len(matches)
if cancelled.Get() { if cancelled.Get() {
return return
} }
countChan <- len(matches) countChan <- len(matches)
} }
sliceMatches := make([]*Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort { if m.sort {
if m.tac { if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches)) sort.Sort(ByRelevanceTac(sliceMatches))
@@ -199,12 +206,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
partialResults := make([][]*Item, numSlices) partialResults := make([][]*Result, numSlices)
for _, _ = range slices { for _ = range slices {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(partialResults, m.sort, m.tac), false return NewMerger(pattern, partialResults, m.sort, m.tac), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search

View File

@@ -2,14 +2,15 @@ package fzf
import "fmt" import "fmt"
// Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false, false) var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
type Merger struct { type Merger struct {
lists [][]*Item pattern *Pattern
merged []*Item lists [][]*Result
merged []*Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
@@ -22,9 +23,10 @@ type Merger struct {
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger { func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
mg := Merger{ mg := Merger{
chunks: chunks, pattern: nil,
tac: tac, chunks: chunks,
count: 0} tac: tac,
count: 0}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += len(*chunk) mg.count += len(*chunk)
@@ -33,10 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern,
lists: lists, lists: lists,
merged: []*Item{}, merged: []*Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
@@ -55,14 +58,14 @@ func (mg *Merger) Length() int {
return mg.count return mg.count
} }
// Get returns the pointer to the Item object indexed by the given integer // Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) *Item { func (mg *Merger) Get(idx int) *Result {
if mg.chunks != nil { if mg.chunks != nil {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
chunk := (*mg.chunks)[idx/chunkSize] chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize] return &Result{item: (*chunk)[idx%chunkSize]}
} }
if mg.sorted { if mg.sorted {
@@ -86,9 +89,9 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Result {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := buildEmptyRank(0) minRank := minRank()
minIdx := -1 minIdx := -1
for listIdx, list := range mg.lists { for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx] cursor := mg.cursors[listIdx]
@@ -97,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
continue continue
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].Rank(false) rank := list[cursor].rank
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank minRank = rank
minIdx = listIdx minIdx = listIdx

View File

@@ -5,6 +5,8 @@ import (
"math/rand" "math/rand"
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assert(t *testing.T, cond bool, msg ...string) { func assert(t *testing.T, cond bool, msg ...string) {
@@ -13,18 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
} }
} }
func randItem() *Item { func randResult() *Result {
str := fmt.Sprintf("%d", rand.Uint32()) str := fmt.Sprintf("%d", rand.Uint32())
offsets := make([]Offset, rand.Int()%3) return &Result{
for idx := range offsets { item: &Item{text: util.RunesToChars([]rune(str))},
sidx := int32(rand.Uint32() % 20) rank: rank{index: rand.Int31()}}
eidx := sidx + int32(rand.Uint32()%20)
offsets[idx] = Offset{sidx, eidx}
}
return &Item{
text: []rune(str),
rank: buildEmptyRank(rand.Int31()),
offsets: offsets}
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
@@ -34,23 +29,23 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]*Item, []*Item) { func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
numLists := 4 numLists := 4
lists := make([][]*Item, numLists) lists := make([][]*Result, numLists)
cnt := 0 cnt := 0
for i := 0; i < numLists; i++ { for i := 0; i < numLists; i++ {
numItems := rand.Int() % 20 numResults := rand.Int() % 20
cnt += numItems cnt += numResults
lists[i] = make([]*Item, numItems) lists[i] = make([]*Result, numResults)
for j := 0; j < numItems; j++ { for j := 0; j < numResults; j++ {
item := randItem() item := randResult()
lists[i][j] = item lists[i][j] = item
} }
if partiallySorted { if partiallySorted {
sort.Sort(ByRelevance(lists[i])) sort.Sort(ByRelevance(lists[i]))
} }
} }
items := []*Item{} items := []*Result{}
for _, list := range lists { for _, list := range lists {
items = append(items, list...) items = append(items, list...)
} }
@@ -62,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(lists, false, false) mg := NewMerger(nil, lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -74,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(lists, true, false) mg := NewMerger(nil, lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -84,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(lists, true, false) mg2 := NewMerger(nil, lists, true, false)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"fmt"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -23,35 +24,47 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform item using index expressions within finder --with-nth=N[,..] Transform the presentation of each line using
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied; when the scores are tied [length|begin|end|index]
[length|begin|end|index] (default: length) (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --bind=KEYBINDS Custom key bindings. Refer to the man page.
--black Use black background
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the --hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--jump-labels=CHARS Label characters for jump and jump-accept
Layout
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
Display
--ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
History
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Preview
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
@@ -82,13 +95,35 @@ type criterion int
const ( const (
byMatchLen criterion = iota byMatchLen criterion = iota
byBonus
byLength byLength
byBegin byBegin
byEnd byEnd
) )
func defaultMargin() [4]string { type sizeSpec struct {
return [4]string{"0", "0", "0", "0"} size float64
percent bool
}
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type windowPosition int
const (
posUp windowPosition = iota
posDown
posLeft
posRight
)
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@@ -112,6 +147,7 @@ type Options struct {
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
InlineInfo bool InlineInfo bool
JumpLabels string
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
@@ -121,24 +157,18 @@ type Options struct {
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string Execmap map[int]string
Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]string Margin [4]sizeSpec
Tabstop int Tabstop int
Version bool Version bool
} }
func defaultTheme() *curses.ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return curses.Dark256
}
return curses.Default16
}
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Fuzzy: true, Fuzzy: true,
@@ -149,17 +179,18 @@ func defaultOptions() *Options {
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Criteria: []criterion{byMatchLen, byLength}, Criteria: []criterion{byMatchLen, byBonus, byLength},
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme(), Theme: curses.EmptyTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
InlineInfo: false, InlineInfo: false,
JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
@@ -169,6 +200,7 @@ func defaultOptions() *Options {
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int]actionType),
Execmap: make(map[int]string), Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
@@ -322,6 +354,12 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.AltZ + int(' ') chord = curses.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = curses.BSpace chord = curses.BSpace
case "alt-enter", "alt-return":
chord = curses.AltEnter
case "alt-space":
chord = curses.AltSpace
case "alt-/":
chord = curses.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = curses.AltBS chord = curses.AltBS
case "tab": case "tab":
@@ -346,12 +384,14 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.SRight chord = curses.SRight
case "double-click": case "double-click":
chord = curses.DoubleClick chord = curses.DoubleClick
case "f10":
chord = curses.F10
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = curses.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a' chord = curses.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1' chord = curses.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 { } else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0]) chord = curses.AltZ + int([]rune(key)[0])
@@ -367,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string {
} }
func parseTiebreak(str string) []criterion { func parseTiebreak(str string) []criterion {
criteria := []criterion{byMatchLen} criteria := []criterion{byMatchLen, byBonus}
hasIndex := false hasIndex := false
hasLength := false hasLength := false
hasBegin := false hasBegin := false
@@ -454,6 +494,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
theme.Match = ansi theme.Match = ansi
case "hl+": case "hl+":
theme.CurrentMatch = ansi theme.CurrentMatch = ansi
case "border":
theme.Border = ansi
case "prompt": case "prompt":
theme.Prompt = ansi theme.Prompt = ansi
case "spinner": case "spinner":
@@ -534,6 +576,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actAbort keymap[key] = actAbort
case "accept": case "accept":
keymap[key] = actAccept keymap[key] = actAccept
case "print-query":
keymap[key] = actPrintQuery
case "backward-char": case "backward-char":
keymap[key] = actBackwardChar keymap[key] = actBackwardChar
case "backward-delete-char": case "backward-delete-char":
@@ -554,6 +598,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actForwardChar keymap[key] = actForwardChar
case "forward-word": case "forward-word":
keymap[key] = actForwardWord keymap[key] = actForwardWord
case "jump":
keymap[key] = actJump
case "jump-accept":
keymap[key] = actJumpAccept
case "kill-line": case "kill-line":
keymap[key] = actKillLine keymap[key] = actKillLine
case "kill-word": case "kill-word":
@@ -594,6 +642,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actPreviousHistory keymap[key] = actPreviousHistory
case "next-history": case "next-history":
keymap[key] = actNextHistory keymap[key] = actNextHistory
case "toggle-preview":
keymap[key] = actTogglePreview
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
default: default:
@@ -649,40 +699,87 @@ func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n") return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
} }
func parseMargin(margin string) [4]string { func parseSize(str string, maxPercent float64, label string) sizeSpec {
margins := strings.Split(margin, ",") var val float64
checked := func(str string) string { percent := strings.HasSuffix(str, "%")
if strings.HasSuffix(str, "%") { if percent {
val := atof(str[:len(str)-1]) val = atof(str[:len(str)-1])
if val < 0 { if val < 0 {
errorExit("margin must be non-negative") errorExit(label + " must be non-negative")
}
if val > 100 {
errorExit("margin too large")
}
} else {
val := atoi(str)
if val < 0 {
errorExit("margin must be non-negative")
}
} }
return str if val > maxPercent {
errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent)))
}
} else {
if strings.Contains(str, ".") {
errorExit(label + " (without %) must be a non-negative integer")
}
val = float64(atoi(str))
if val < 0 {
errorExit(label + " must be non-negative")
}
}
return sizeSpec{val, percent}
}
func parsePreviewWindow(opts *previewOpts, input string) {
layout := input
opts.hidden = false
if strings.HasSuffix(layout, ":hidden") {
opts.hidden = true
layout = strings.TrimSuffix(layout, ":hidden")
}
tokens := strings.Split(layout, ":")
if len(tokens) == 0 || len(tokens) > 2 {
errorExit("invalid window layout: " + input)
}
if len(tokens) > 1 {
opts.size = parseSize(tokens[1], 99, "window size")
} else {
opts.size = sizeSpec{50, true}
}
if !opts.size.percent && opts.size.size > 0 {
// Adjust size for border
opts.size.size += 2
}
switch tokens[0] {
case "up":
opts.position = posUp
case "down":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
errorExit("invalid window position: " + input)
}
}
func parseMargin(margin string) [4]sizeSpec {
margins := strings.Split(margin, ",")
checked := func(str string) sizeSpec {
return parseSize(str, 49, "margin")
} }
switch len(margins) { switch len(margins) {
case 1: case 1:
m := checked(margins[0]) m := checked(margins[0])
return [4]string{m, m, m, m} return [4]sizeSpec{m, m, m, m}
case 2: case 2:
tb := checked(margins[0]) tb := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
return [4]string{tb, rl, tb, rl} return [4]sizeSpec{tb, rl, tb, rl}
case 3: case 3:
t := checked(margins[0]) t := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
b := checked(margins[2]) b := checked(margins[2])
return [4]string{t, rl, b, rl} return [4]sizeSpec{t, rl, b, rl}
case 4: case 4:
return [4]string{ return [4]sizeSpec{
checked(margins[0]), checked(margins[1]), checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])} checked(margins[2]), checked(margins[3])}
default: default:
@@ -714,6 +811,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.History.maxSize = historyMax opts.History.maxSize = historyMax
} }
} }
validateJumpLabels := false
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -745,7 +843,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
opts.Theme = defaultTheme() opts.Theme = curses.EmptyTheme()
} else { } else {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
@@ -805,6 +903,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
opts.InlineInfo = false opts.InlineInfo = false
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
case "-1", "--select-1": case "-1", "--select-1":
opts.Select1 = true opts.Select1 = true
case "+1", "--no-select-1": case "+1", "--no-select-1":
@@ -844,6 +945,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines": case "--header-lines":
opts.HeaderLines = atoi( opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required")) nextString(allArgs, &i, "number of header lines required"))
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]"))
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--margin": case "--margin":
@@ -886,12 +994,18 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value) opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match {
parsePreviewWindow(&opts.Preview, value)
} else if match, value := optString(arg, "--margin="); match { } else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match { } else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value) opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--jump-labels="); match {
opts.JumpLabels = value
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
@@ -909,6 +1023,18 @@ func parseOptions(opts *Options, allArgs []string) {
if opts.Tabstop < 1 { if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer") errorExit("tab stop must be a positive integer")
} }
if len(opts.JumpLabels) == 0 {
errorExit("empty jump labels")
}
if validateJumpLabels {
for _, r := range opts.JumpLabels {
if r < 32 || r > 126 {
errorExit("non-ascii jump labels are not allowed")
}
}
}
} }
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
@@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) { func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*") delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
if delim.regex != nil || if delim.regex != nil ||
string(tokens[0].text) != "-*" || tokens[0].text.ToString() != "-*" ||
string(tokens[1].text) != "--*" || tokens[1].text.ToString() != "--*" ||
string(tokens[2].text) != "---*" || tokens[2].text.ToString() != "---*" ||
string(tokens[3].text) != "*" || tokens[3].text.ToString() != "*" ||
string(tokens[4].text) != "---" { tokens[4].text.ToString() != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens)) t.Errorf("%s %s %d", delim, tokens, len(tokens))
} }
} }
func TestDelimiterRegexRegex(t *testing.T) { func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*") delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
if delim.str != nil || if delim.str != nil ||
string(tokens[0].text) != "-*--*" || tokens[0].text.ToString() != "-*--*" ||
string(tokens[1].text) != "---*" || tokens[1].text.ToString() != "---*" ||
string(tokens[2].text) != "*---" { tokens[2].text.ToString() != "*---" {
t.Errorf("%s %d", tokens, len(tokens)) t.Errorf("%s %d", tokens, len(tokens))
} }
} }
@@ -123,14 +124,14 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "")
check := func(i int, s string) { check := func(i int, s string) {
if pairs[i] != s { if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[i], s)
} }
} }
if len(pairs) != 9 { if len(pairs) != 11 {
t.Error(9) t.Error(11)
} }
check(curses.CtrlZ, "ctrl-z") check(curses.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z") check(curses.AltZ, "alt-z")
@@ -141,6 +142,8 @@ func TestParseKeys(t *testing.T) {
check(curses.CtrlA+'g'-'a', "ctrl-G") check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J") check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g") check(curses.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter")
check(curses.AltSpace, "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
@@ -350,14 +353,14 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
} }
func TestToggle(t *testing.T) { func optsFor(words ...string) *Options {
optsFor := func(words ...string) *Options { opts := defaultOptions()
opts := defaultOptions() parseOptions(opts, words)
parseOptions(opts, words) postProcessOptions(opts)
postProcessOptions(opts) return opts
return opts }
}
func TestToggle(t *testing.T) {
opts := optsFor() opts := optsFor()
if opts.ToggleSort { if opts.ToggleSort {
t.Error() t.Error()
@@ -373,3 +376,31 @@ func TestToggle(t *testing.T) {
t.Error() t.Error()
} }
} }
func TestPreviewOpts(t *testing.T) {
opts := optsFor()
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posRight &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error()
}
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden")
if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true &&
opts.Preview.position == posLeft &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posDown &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error(opts.Preview)
}
}

View File

@@ -2,7 +2,6 @@ package fzf
import ( import (
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
@@ -49,7 +48,7 @@ type Pattern struct {
cacheable bool cacheable bool
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, bool, []rune, []rune) algo.Result procFun map[termType]func(bool, bool, util.Chars, []rune) algo.Result
} }
var ( var (
@@ -76,7 +75,7 @@ func clearChunkCache() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern { cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
if extended { if extended {
@@ -90,7 +89,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return cached return cached
} }
caseSensitive, cacheable := true, true caseSensitive := true
termSets := []termSet{} termSets := []termSet{}
if extended { if extended {
@@ -100,7 +99,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
for idx, term := range termSet { for idx, term := range termSet {
// If the query contains inverse search terms or OR operators, // If the query contains inverse search terms or OR operators,
// we cannot cache the search scope // we cannot cache the search scope
if idx > 0 || term.inv { if !cacheable || idx > 0 || term.inv {
cacheable = false cacheable = false
break Loop break Loop
} }
@@ -125,7 +124,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)} procFun: make(map[termType]func(bool, bool, util.Chars, []rune) algo.Result)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -235,9 +234,7 @@ func (p *Pattern) CacheKey() string {
} }
// Match returns the list of matches Items in the given Chunk // Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk) []*Item { func (p *Pattern) Match(chunk *Chunk) []*Result {
space := chunk
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
@@ -246,7 +243,8 @@ func (p *Pattern) Match(chunk *Chunk) []*Item {
} }
} }
// ChunkCache: Prefix/suffix match // Prefix/suffix cache
var space []*Result
Loop: Loop:
for idx := 1; idx < len(cacheKey); idx++ { for idx := 1; idx < len(cacheKey); idx++ {
// [---------| ] | [ |---------] // [---------| ] | [ |---------]
@@ -256,14 +254,13 @@ Loop:
suffix := cacheKey[idx:] suffix := cacheKey[idx:]
for _, substr := range [2]*string{&prefix, &suffix} { for _, substr := range [2]*string{&prefix, &suffix} {
if cached, found := _cache.Find(chunk, *substr); found { if cached, found := _cache.Find(chunk, *substr); found {
cachedChunk := Chunk(cached) space = cached
space = &cachedChunk
break Loop break Loop
} }
} }
} }
matches := p.matchChunk(space) matches := p.matchChunk(chunk, space)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@@ -271,20 +268,19 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result {
matches := []*Item{} matches := []*Result{}
if !p.extended {
if space == nil {
for _, item := range *chunk { for _, item := range *chunk {
offset, bonus := p.basicMatch(item) if match, _ := p.MatchItem(item); match != nil {
if sidx := offset[0]; sidx >= 0 { matches = append(matches, match)
matches = append(matches,
dupItem(item, []Offset{offset}, bonus))
} }
} }
} else { } else {
for _, item := range *chunk { for _, result := range space {
if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { if match, _ := p.MatchItem(result.item); match != nil {
matches = append(matches, dupItem(item, offsets, bonus)) matches = append(matches, match)
} }
} }
} }
@@ -292,29 +288,22 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
} }
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item) (*Result, []Offset) {
if !p.extended { if p.extended {
offset, _ := p.basicMatch(item) if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) {
sidx := offset[0] return buildResult(item, offsets, bonus, trimLen), offsets
return sidx >= 0 }
return nil, nil
} }
offsets, _ := p.extendedMatch(item) offset, bonus, trimLen := p.basicMatch(item)
return len(offsets) == len(p.termSets) if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
return buildResult(item, offsets, bonus, trimLen), offsets
}
return nil, nil
} }
func dupItem(item *Item, offsets []Offset, bonus int32) *Item { func (p *Pattern) basicMatch(item *Item) (Offset, int, int) {
sort.Sort(ByOrder(offsets))
return &Item{
text: item.text,
origText: item.origText,
transformed: item.transformed,
offsets: offsets,
bonus: bonus,
colors: item.colors,
rank: buildEmptyRank(item.Index())}
}
func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
input := p.prepareInput(item) input := p.prepareInput(item)
if p.fuzzy { if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
@@ -322,33 +311,39 @@ func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
var totalBonus int32 var totalBonus int
var totalTrimLen int
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
var offset *Offset var offset Offset
var bonus int32 var bonus int
var trimLen int
matched := false
for _, term := range termSet { for _, term := range termSet {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
if sidx := off[0]; sidx >= 0 { if sidx := off[0]; sidx >= 0 {
if term.inv { if term.inv {
continue continue
} }
offset, bonus = &off, pen offset, bonus, trimLen = off, pen, tLen
matched = true
break break
} else if term.inv { } else if term.inv {
offset, bonus = &Offset{0, 0, 0}, 0 offset, bonus, trimLen = Offset{0, 0}, 0, 0
matched = true
continue continue
} }
} }
if offset != nil { if matched {
offsets = append(offsets, *offset) offsets = append(offsets, offset)
totalBonus += bonus totalBonus += bonus
totalTrimLen += trimLen
} }
} }
return offsets, totalBonus return offsets, totalBonus, totalTrimLen
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) prepareInput(item *Item) []Token {
@@ -357,26 +352,24 @@ func (p *Pattern) prepareInput(item *Item) []Token {
} }
var ret []Token var ret []Token
if len(p.nth) > 0 { if len(p.nth) == 0 {
ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}}
} else {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else {
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result, func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := int32(part.prefixLength) if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 {
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength
var sidx int32 = res.Start + prefixLength eidx := int32(res.End) + part.prefixLength
var eidx int32 = res.End + prefixLength return Offset{sidx, eidx}, res.Bonus, int(part.trimLength)
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
} }
} }
// TODO: math.MaxUint16 return Offset{-1, -1}, 0, -1
return Offset{-1, -1, -1}, 0.0
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
@@ -68,10 +69,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, pattern := BuildPattern(true, true, CaseSmart, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
res := algo.ExactMatchNaive( res := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text)
if res.Start != 7 || res.End != 10 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
@@ -80,11 +81,11 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int32, eidxExpected int32) { match := func(str string, sidxExpected int, eidxExpected int) {
res := algo.EqualMatch( res := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text)
if res.Start != sidxExpected || res.End != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
@@ -96,17 +97,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) pat1 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -119,31 +120,37 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi") origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: []rune("junegunn"), text: util.RunesToChars([]rune("junegunn")),
origText: &origRunes, origText: &origBytes,
transformed: trans}, transformed: trans},
} }
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk, nil) // No cache
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || !reflect.DeepEqual(matches[0].item.transformed, trans) {
!reflect.DeepEqual(matches[0].transformed, trans) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
match, offsets := pattern.MatchItem(chunk[0])
if match.item.text.ToString() != "junegunn" || string(*match.item.origText) != "junegunn.choi" ||
offsets[0][0] != 0 || offsets[0][1] != 5 ||
!reflect.DeepEqual(match.item.transformed, trans) {
t.Error("Invalid match result", match)
}
} }
} }
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr)) pat := BuildPattern(true, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }

View File

@@ -34,7 +34,7 @@ func (r *Reader) feed(src io.Reader) {
if r.delimNil { if r.delimNil {
delim = '\000' delim = '\000'
} }
reader := bufio.NewReader(src) reader := bufio.NewReaderSize(src, readerBufferSize)
for { for {
// ReadBytes returns err != nil if and only if the returned data does not // ReadBytes returns err != nil if and only if the returned data does not
// end in delim. // end in delim.

258
src/result.go Normal file
View File

@@ -0,0 +1,258 @@
package fzf
import (
"math"
"sort"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
index int32
}
type rank struct {
// byMatchLen, byBonus, ...
points [5]uint16
index int32
}
type Result struct {
item *Item
rank rank
}
func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item, rank: rank{index: item.index}}
matchlen := 0
prevEnd := 0
minBegin := math.MaxInt32
numChars := item.text.Length()
for _, offset := range offsets {
begin := int(offset[0])
end := int(offset[1])
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
for idx, criterion := range sortCriteria {
var val uint16
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxUint16
} else {
val = util.AsUint16(matchlen)
}
case byBonus:
// Higher is better
val = math.MaxUint16 - util.AsUint16(bonus)
case byLength:
// If offsets is empty, trimLen will be 0, but we don't care
val = util.AsUint16(trimLen)
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = util.AsUint16(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = util.AsUint16(1 + numChars - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
result.rank.points[idx] = val
}
return &result
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
// Index returns ordinal index of the Item
func (result *Result) Index() int32 {
return result.item.index
}
func minRank() rank {
return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset {
itemColors := result.item.Colors()
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range matchOffsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range itemColors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var colors []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return colors
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Result
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Result
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
}
func compareRanks(irank rank, jrank rank, tac bool) bool {
for idx := 0; idx < 5; idx++ {
left := irank.points[idx]
right := jrank.points[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank.index <= jrank.index) != tac
}

115
src/result_test.go Normal file
View File

@@ -0,0 +1,115 @@
package fzf
import (
"math"
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
Offset{1, 3}, Offset{2, 9}}
sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
offsets[3][0] != 3 || offsets[3][1] != 5 {
t.Error("Invalid order:", offsets)
}
}
func TestRankComparison(t *testing.T) {
rank := func(vals ...uint16) rank {
return rank{
points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]},
index: int32(vals[4])}
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order")
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestResultRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byBonus, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3)
if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 {
t.Error(item1.rank)
}
// Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3)
items := []*Result{item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item1 {
t.Error(items)
}
items = []*Result{item2, item1, item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item2 ||
items[2] != item1 || items[3] != item1 {
t.Error(items, item1, item1.item.index, item2, item2.item.index)
}
// Sort by relevance
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
items = []*Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if items[0] != item6 || items[1] != item4 ||
items[2] != item5 || items[3] != item3 ||
items[4] != item2 || items[5] != item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
item := Result{
item: &Item{
colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
colors := item.colorOffsets(offsets, 99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -7,7 +7,6 @@ import (
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -19,6 +18,14 @@ import (
"github.com/junegunn/go-runewidth" "github.com/junegunn/go-runewidth"
) )
type jumpMode int
const (
jumpDisabled jumpMode = iota
jumpEnabled
jumpAcceptEnabled
)
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
@@ -45,14 +52,22 @@ type Terminal struct {
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
margin [4]string margin [4]sizeSpec
marginInt [4]int window *C.Window
bwindow *C.Window
pwindow *C.Window
count int count int
progress int progress int
reading bool reading bool
jumping jumpMode
jumpLabels string
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts
previewing bool
previewTxt string
previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func()
@@ -62,11 +77,16 @@ type Terminal struct {
type selectedItem struct { type selectedItem struct {
at time.Time at time.Time
text *string text string
} }
type byTimeOrder []selectedItem type byTimeOrder []selectedItem
type previewRequest struct {
ok bool
str string
}
func (a byTimeOrder) Len() int { func (a byTimeOrder) Len() int {
return len(a) return len(a)
} }
@@ -88,9 +108,13 @@ const (
reqInfo reqInfo
reqHeader reqHeader
reqList reqList
reqJump
reqRefresh reqRefresh
reqRedraw reqRedraw
reqClose reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqQuit reqQuit
) )
@@ -132,7 +156,11 @@ const (
actUp actUp
actPageUp actPageUp
actPageDown actPageDown
actJump
actJumpAccept
actPrintQuery
actToggleSort actToggleSort
actTogglePreview
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
@@ -205,6 +233,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else { } else {
delay = initialDelay delay = initialDelay
} }
var previewBox *util.EventBox
if len(opts.Preview.command) > 0 {
previewBox = util.NewEventBox()
}
return &Terminal{ return &Terminal{
initDelay: delay, initDelay: delay,
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
@@ -227,15 +259,20 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
reading: true, reading: true,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
preview: opts.Preview,
previewing: previewBox != nil && !opts.Preview.hidden,
previewTxt: "",
previewBox: previewBox,
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
suppress: true, suppress: true,
@@ -315,12 +352,12 @@ func (t *Terminal) output() bool {
if !found { if !found {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString(t.ansi)) fmt.Println(t.current())
found = true found = true
} }
} else { } else {
for _, sel := range t.sortSelected() { for _, sel := range t.sortSelected() {
fmt.Println(*sel.text) fmt.Println(sel.text)
} }
} }
return found return found
@@ -355,56 +392,113 @@ func displayWidth(runes []rune) int {
return l return l
} }
const minWidth = 16 const (
const minHeight = 4 minWidth = 16
minHeight = 4
)
func (t *Terminal) calculateMargins() { func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
max := base - margin
if size.percent {
return util.Constrain(int(float64(base)*0.01*size.size), minSize, max)
}
return util.Constrain(int(size.size), minSize, max)
}
func (t *Terminal) resizeWindows() {
screenWidth := C.MaxX() screenWidth := C.MaxX()
screenHeight := C.MaxY() screenHeight := C.MaxY()
for idx, str := range t.margin { marginInt := [4]int{}
if str == "0" { for idx, sizeSpec := range t.margin {
t.marginInt[idx] = 0 if sizeSpec.percent {
} else if strings.HasSuffix(str, "%") { var max float64
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 { if idx%2 == 0 {
val = float64(screenHeight) max = float64(screenHeight)
} else { } else {
val = float64(screenWidth) max = float64(screenWidth)
} }
t.marginInt[idx] = int(val * num * 0.01) marginInt[idx] = int(max * sizeSpec.size * 0.01)
} else { } else {
num, _ := strconv.Atoi(str) marginInt[idx] = int(sizeSpec.size)
t.marginInt[idx] = num
} }
} }
adjust := func(idx1 int, idx2 int, max int, min int) { adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min { if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2] margin := marginInt[idx1] + marginInt[idx2]
if max-margin < min { if max-margin < min {
desired := max - min desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin marginInt[idx1] = desired * marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin marginInt[idx2] = desired * marginInt[idx2] / margin
} }
} }
} }
adjust(1, 3, screenWidth, minWidth) minAreaWidth := minWidth
adjust(0, 2, screenHeight, minHeight) minAreaHeight := minHeight
if t.isPreviewEnabled() {
switch t.preview.position {
case posUp, posDown:
minAreaHeight *= 2
case posLeft, posRight:
minAreaWidth *= 2
}
}
adjust(1, 3, screenWidth, minAreaWidth)
adjust(0, 2, screenHeight, minAreaHeight)
if t.window != nil {
t.window.Close()
}
if t.bwindow != nil {
t.bwindow.Close()
t.pwindow.Close()
}
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = C.NewWindow(y, x, w, h, true)
t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
t.window = C.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false)
}
} }
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY()
if !t.reverse { if !t.reverse {
y = maxy - y - 1 - t.marginInt[2] y = t.window.Height - y - 1
} else {
y += t.marginInt[0]
} }
if clear { if clear {
C.MoveAndClear(y, x) t.window.MoveAndClear(y, x)
} else { } else {
C.Move(y, x) t.window.Move(y, x)
} }
} }
@@ -414,24 +508,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
t.move(0, 0, true) t.move(0, 0, true)
C.CPrint(C.ColPrompt, true, t.prompt) t.window.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input)) t.window.CPrint(C.ColNormal, true, string(t.input))
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading { if t.reading {
C.CPrint(C.ColSpinner, true, " < ") t.window.CPrint(C.ColSpinner, true, " < ")
} else { } else {
C.CPrint(C.ColPrompt, true, " < ") t.window.CPrint(C.ColPrompt, true, " < ")
} }
} else { } else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx]) t.window.CPrint(C.ColSpinner, true, _spinner[idx])
} }
t.move(1, 2, false) t.move(1, 2, false)
} }
@@ -450,18 +544,14 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
C.CPrint(C.ColInfo, false, output) t.window.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header) == 0 {
return return
} }
max := t.maxHeight() max := t.window.Height
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
line := idx + 2 line := idx + 2
@@ -471,15 +561,14 @@ func (t *Terminal) printHeader() {
if line >= max { if line >= max {
continue continue
} }
trimmed, colors, newState := extractColor(lineStr, state) trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState state = newState
item := &Item{ item := &Item{
text: []rune(trimmed), text: util.RunesToChars([]rune(trimmed)),
colors: colors, colors: colors}
rank: buildEmptyRank(0)}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false) t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false)
} }
} }
@@ -495,29 +584,39 @@ func (t *Terminal) printList() {
} }
t.move(line, 0, true) t.move(line, 0, true)
if i < count { if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset)
} }
} }
} }
func (t *Terminal) printItem(item *Item, current bool) { func (t *Terminal) printItem(result *Result, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " "
if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) {
// Striped
current = i%2 == 0
label = t.jumpLabels[i : i+1]
}
} else if current {
label = ">"
}
t.window.CPrint(C.ColCursor, true, label)
if current { if current {
C.CPrint(C.ColCursor, true, ">")
if selected { if selected {
C.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, true, ">")
} else { } else {
C.CPrint(C.ColCurrent, true, " ") t.window.CPrint(C.ColCurrent, true, " ")
} }
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
C.CPrint(C.ColCursor, true, " ")
if selected { if selected {
C.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, true, ">")
} else { } else {
C.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(item, false, 0, C.ColMatch, false) t.printHighlighted(result, false, 0, C.ColMatch, false)
} }
} }
@@ -557,29 +656,43 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed return runes, trimmed
} }
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { func overflow(runes []rune, max int) bool {
var maxe int l := 0
for _, offset := range item.offsets { for _, r := range runes {
maxe = util.Max(maxe, int(offset[1])) l += runeWidth(r, l)
if l > max {
return true
}
} }
return false
}
func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) {
item := result.item
// Overflow // Overflow
text := make([]rune, len(item.text)) text := make([]rune, item.text.Length())
copy(text, item.text) copy(text, item.text.ToRunes())
offsets := item.colorOffsets(col2, bold, current) matchOffsets := []Offset{}
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] if t.merger.pattern != nil {
_, matchOffsets = t.merger.pattern.MatchItem(item)
}
var maxe int
for _, offset := range matchOffsets {
maxe = util.Max(maxe, int(offset[1]))
}
offsets := result.colorOffsets(matchOffsets, col2, bold, current)
maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text) if overflow(text, maxWidth) {
if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
// Stri.. // Stri..
matchEndWidth := displayWidth(text[:maxe]) if !overflow(text[:maxe], maxWidth-2) {
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2) text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...) text = append(text, []rune("..")...)
} else { } else {
// Stri.. // Stri..
if matchEndWidth < fullWidth-2 { if overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...) text = append(text[:maxe], []rune("..")...)
} }
// ..ri.. // ..ri..
@@ -617,11 +730,11 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
e := util.Constrain32(offset.offset[1], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr) t.window.CPrint(col1, bold, substr)
if b < e { if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth) substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(offset.color, offset.bold, substr) t.window.CPrint(offset.color, offset.bold, substr)
} }
index = e index = e
@@ -631,10 +744,20 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
if index < maxOffset { if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr) t.window.CPrint(col1, bold, substr)
} }
} }
func (t *Terminal) printPreview() {
t.pwindow.Erase()
extractColor(t.previewTxt, nil, func(str string, ansi *ansiState) bool {
if ansi != nil && ansi.colored() {
return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold)
}
return t.pwindow.Fill(str)
})
}
func processTabs(runes []rune, prefixWidth int) (string, int) { func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer var strbuf bytes.Buffer
l := prefixWidth l := prefixWidth
@@ -651,16 +774,24 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
} }
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.calculateMargins() t.resizeWindows()
t.printList() t.printList()
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
if t.isPreviewEnabled() {
t.printPreview()
}
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
C.Refresh() if t.isPreviewEnabled() {
t.bwindow.Refresh()
t.pwindow.Refresh()
}
t.window.Refresh()
C.DoUpdate()
} }
} }
@@ -717,10 +848,10 @@ func keyMatch(key int, event C.Event) bool {
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
return fmt.Sprintf("%q", entry) return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
func executeCommand(template string, replacement string) { func (t *Terminal) executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1) command := strings.Replace(template, "{}", replacement, -1)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@@ -728,7 +859,19 @@ func executeCommand(template string, replacement string) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
C.Endwin() C.Endwin()
cmd.Run() cmd.Run()
C.Refresh() t.refresh()
}
func (t *Terminal) hasPreviewWindow() bool {
return t.previewBox != nil
}
func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewing
}
func (t *Terminal) current() string {
return t.merger.Get(t.cy).item.AsString(t.ansi)
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@@ -753,10 +896,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins() t.resizeWindows()
t.printPrompt() t.printPrompt()
t.placeCursor() t.placeCursor()
C.Refresh() t.refresh()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
t.mutex.Unlock() t.mutex.Unlock()
@@ -781,6 +924,31 @@ func (t *Terminal) Loop() {
}() }()
} }
if t.hasPreviewWindow() {
go func() {
for {
request := previewRequest{false, ""}
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request = value.(previewRequest)
}
}
events.Clear()
})
if request.ok {
command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1)
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
} else {
t.reqBox.Set(reqPreviewDisplay, "")
}
}
}()
}
exit := func(code int) { exit := func(code int) {
if code <= exitNoMatch && t.history != nil { if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
@@ -789,11 +957,12 @@ func (t *Terminal) Loop() {
} }
go func() { go func() {
focused := previewRequest{false, ""}
for { for {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
t.mutex.Lock() t.mutex.Lock()
for req := range *events { for req, value := range *events {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
@@ -804,6 +973,24 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length()
var currentFocus previewRequest
if cnt > 0 && cnt > t.cy {
currentFocus = previewRequest{true, t.current()}
} else {
currentFocus = previewRequest{false, ""}
}
if currentFocus != focused {
focused = currentFocus
if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused)
}
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
}
t.printList()
case reqHeader: case reqHeader:
t.printHeader() t.printHeader()
case reqRefresh: case reqRefresh:
@@ -819,6 +1006,13 @@ func (t *Terminal) Loop() {
exit(exitOk) exit(exitOk)
} }
exit(exitNoMatch) exit(exitNoMatch)
case reqPreviewDisplay:
t.previewTxt = value.(string)
t.printPreview()
case reqPrintQuery:
C.Close()
fmt.Println(string(t.input))
exit(exitOk)
case reqQuit: case reqQuit:
C.Close() C.Close()
exit(exitInterrupt) exit(exitInterrupt)
@@ -848,13 +1042,13 @@ func (t *Terminal) Loop() {
} }
selectItem := func(item *Item) bool { selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found { if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)} t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
return true return true
} }
return false return false
} }
toggleY := func(y int) { toggleY := func(y int) {
item := t.merger.Get(y) item := t.merger.Get(y).item
if !selectItem(item) { if !selectItem(item) {
delete(t.selected, item.Index()) delete(t.selected, item.Index())
} }
@@ -879,22 +1073,32 @@ func (t *Terminal) Loop() {
case actIgnore: case actIgnore:
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy).item
executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
} }
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { if len(t.selected) > 0 {
sels := make([]string, len(t.selected)) sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() { for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text) sels[i] = quoteEntry(sel.text)
} }
executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else { } else {
return doAction(actExecute, mapkey) return doAction(actExecute, mapkey)
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actTogglePreview:
if t.hasPreviewWindow() {
t.previewing = !t.previewing
t.resizeWindows()
cnt := t.merger.Length()
if t.previewing && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()})
}
req(reqList, reqInfo)
}
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
@@ -906,6 +1110,8 @@ func (t *Terminal) Loop() {
if t.cx > 0 { if t.cx > 0 {
t.cx-- t.cx--
} }
case actPrintQuery:
req(reqPrintQuery)
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
@@ -936,7 +1142,7 @@ func (t *Terminal) Loop() {
case actSelectAll: case actSelectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i) item := t.merger.Get(i).item
selectItem(item) selectItem(item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)
@@ -1017,6 +1223,12 @@ func (t *Terminal) Loop() {
case actPageDown: case actPageDown:
t.vmove(-(t.maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case actJump:
t.jumping = jumpEnabled
req(reqJump)
case actJumpAccept:
t.jumping = jumpAcceptEnabled
req(reqJump)
case actBackwardWord: case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord: case actForwardWord:
@@ -1054,20 +1266,19 @@ func (t *Terminal) Loop() {
mx, my := me.X, me.Y mx, my := me.X, me.Y
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
if t.multi && me.Mod { if t.multi && me.Mod {
toggle() toggle()
} }
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} }
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && } else if t.window.Enclose(my, mx) {
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { mx -= t.window.Left
mx -= t.marginInt[3] my -= t.window.Top
my -= t.marginInt[0]
mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse { if !t.reverse {
my = t.maxHeight() - my - 1 my = t.window.Height - my - 1
} }
min := 2 + len(t.header) min := 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
@@ -1096,18 +1307,32 @@ func (t *Terminal) Loop() {
} }
return true return true
} }
action := t.keymap[event.Type] changed := false
mapkey := event.Type mapkey := event.Type
if event.Type == C.Rune { if t.jumping == jumpDisabled {
mapkey = int(event.Char) + int(C.AltZ) action := t.keymap[mapkey]
if act, prs := t.keymap[mapkey]; prs { if mapkey == C.Rune {
action = act mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
} }
if !doAction(action, mapkey) {
continue
}
changed = string(previousInput) != string(t.input)
} else {
if mapkey == C.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled {
req(reqClose)
}
}
}
t.jumping = jumpDisabled
req(reqList)
} }
if !doAction(action, mapkey) {
continue
}
changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed { if changed {
@@ -1160,7 +1385,7 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header) max := t.window.Height - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
max++ max++
} }

View File

@@ -18,9 +18,9 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length // Token contains the tokenized part of the strings and its prefix length
type Token struct { type Token struct {
text []rune text *util.Chars
prefixLength int prefixLength int32
trimLength int trimLength int32
} }
// Delimiter for tokenizing the input // Delimiter for tokenizing the input
@@ -75,15 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens [][]rune, begin int) []Token { func withPrefixLengths(tokens []util.Chars, begin int) []Token {
ret := make([]Token, len(tokens)) ret := make([]Token, len(tokens))
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take // NOTE: &tokens[idx] instead of &tokens
// the pointer to it ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())}
ret[idx] = Token{token, prefixLength, util.TrimLen(token)} prefixLength += token.Length()
prefixLength += len(token)
} }
return ret return ret
} }
@@ -94,59 +93,60 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input []rune) ([][]rune, int) { func awkTokenizer(input util.Chars) ([]util.Chars, int) {
// 9, 32 // 9, 32
ret := [][]rune{} ret := []util.Chars{}
str := []rune{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
for _, r := range input { numChars := input.Length()
begin := 0
end := 0
for idx := 0; idx < numChars; idx++ {
r := input.Get(idx)
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
if white { if white {
prefixLength++ prefixLength++
} else { } else {
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = append(str, r)
} }
case awkBlack: case awkBlack:
str = append(str, r) end = idx + 1
if white { if white {
state = awkWhite state = awkWhite
} }
case awkWhite: case awkWhite:
if white { if white {
str = append(str, r) end = idx + 1
} else { } else {
ret = append(ret, str) ret = append(ret, input.Slice(begin, end))
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = []rune{r}
} }
} }
} }
if len(str) > 0 { if begin < end {
ret = append(ret, str) ret = append(ret, input.Slice(begin, end))
} }
return ret, prefixLength return ret, prefixLength
} }
// Tokenize tokenizes the given string with the delimiter // Tokenize tokenizes the given string with the delimiter
func Tokenize(runes []rune, delimiter Delimiter) []Token { func Tokenize(text util.Chars, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil { if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*) // AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(runes) tokens, prefixLength := awkTokenizer(text)
return withPrefixLengths(tokens, prefixLength) return withPrefixLengths(tokens, prefixLength)
} }
var tokens []string
if delimiter.str != nil { if delimiter.str != nil {
tokens = strings.Split(string(runes), *delimiter.str) return withPrefixLengths(text.Split(*delimiter.str), 0)
for i := 0; i < len(tokens)-1; i++ { }
tokens[i] = tokens[i] + *delimiter.str
} // FIXME performance
} else if delimiter.regex != nil { var tokens []string
str := string(runes) if delimiter.regex != nil {
str := text.ToString()
for len(str) > 0 { for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str) loc := delimiter.regex.FindStringIndex(str)
if loc == nil { if loc == nil {
@@ -157,9 +157,9 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
str = str[last:] str = str[last:]
} }
} }
asRunes := make([][]rune, len(tokens)) asRunes := make([]util.Chars, len(tokens))
for i, token := range tokens { for i, token := range tokens {
asRunes[i] = []rune(token) asRunes[i] = util.RunesToChars([]rune(token))
} }
return withPrefixLengths(asRunes, 0) return withPrefixLengths(asRunes, 0)
} }
@@ -167,15 +167,7 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
func joinTokens(tokens []Token) []rune { func joinTokens(tokens []Token) []rune {
ret := []rune{} ret := []rune{}
for _, token := range tokens { for _, token := range tokens {
ret = append(ret, token.text...) ret = append(ret, token.text.ToRunes()...)
}
return ret
}
func joinTokensAsRunes(tokens []Token) []rune {
ret := []rune{}
for _, token := range tokens {
ret = append(ret, token.text...)
} }
return ret return ret
} }
@@ -185,19 +177,20 @@ func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth)) transTokens := make([]Token, len(withNth))
numTokens := len(tokens) numTokens := len(tokens)
for idx, r := range withNth { for idx, r := range withNth {
part := []rune{} parts := []*util.Chars{}
minIdx := 0 minIdx := 0
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
part = append(part, joinTokensAsRunes(tokens)...) chars := util.RunesToChars(joinTokens(tokens))
parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {
idx += numTokens + 1 idx += numTokens + 1
} }
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
minIdx = idx - 1 minIdx = idx - 1
part = append(part, tokens[idx-1].text...) parts = append(parts, tokens[idx-1].text)
} }
} }
} else { } else {
@@ -224,17 +217,32 @@ func Transform(tokens []Token, withNth []Range) []Token {
minIdx = util.Max(0, begin-1) minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ { for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
part = append(part, tokens[idx-1].text...) parts = append(parts, tokens[idx-1].text)
} }
} }
} }
var prefixLength int // Merge multiple parts
var merged util.Chars
switch len(parts) {
case 0:
merged = util.RunesToChars([]rune{})
case 1:
merged = *parts[0]
default:
runes := []rune{}
for _, part := range parts {
runes = append(runes, part.ToRunes()...)
}
merged = util.RunesToChars(runes)
}
var prefixLength int32
if minIdx < numTokens { if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength prefixLength = tokens[minIdx].prefixLength
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)} transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())}
} }
return transTokens return transTokens
} }

View File

@@ -1,6 +1,10 @@
package fzf package fzf
import "testing" import (
"testing"
"github.com/junegunn/fzf/src/util"
)
func TestParseRange(t *testing.T) { func TestParseRange(t *testing.T) {
{ {
@@ -43,23 +47,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) { func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: def: ghi "
tokens := Tokenize([]rune(input), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize([]rune(input), delimiterRegexp(":")) tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter regex // With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }
@@ -67,7 +71,7 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) { func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl" input := " abc: def: ghi: jkl"
{ {
tokens := Tokenize([]rune(input), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
@@ -80,25 +84,25 @@ func TestTransform(t *testing.T) {
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 || tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 { tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
} }
{ {
tokens := Tokenize([]rune(input), delimiterRegexp(":")) tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" || if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 || tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 { tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }

View File

@@ -2,6 +2,7 @@
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient # http://www.rubydoc.info/github/rest-client/rest-client/RestClient
require 'rest_client' require 'rest_client'
require 'json'
if ARGV.length < 3 if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>" puts "usage: #$0 <token> <version> <files...>"

156
src/util/chars.go Normal file
View File

@@ -0,0 +1,156 @@
package util
import (
"unicode/utf8"
)
type Chars struct {
runes []rune
bytes []byte
}
// ToChars converts byte array into rune array
func ToChars(bytea []byte) Chars {
var runes []rune
ascii := true
numBytes := len(bytea)
for i := 0; i < numBytes; {
if bytea[i] < utf8.RuneSelf {
if !ascii {
runes = append(runes, rune(bytea[i]))
}
i++
} else {
if ascii {
ascii = false
runes = make([]rune, i, numBytes)
for j := 0; j < i; j++ {
runes[j] = rune(bytea[j])
}
}
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
if ascii {
return Chars{bytes: bytea}
}
return Chars{runes: runes}
}
func RunesToChars(runes []rune) Chars {
return Chars{runes: runes}
}
func (chars *Chars) Get(i int) rune {
if chars.runes != nil {
return chars.runes[i]
}
return rune(chars.bytes[i])
}
func (chars *Chars) Length() int {
if chars.runes != nil {
return len(chars.runes)
}
return len(chars.bytes)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int {
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) ToString() string {
if chars.runes != nil {
return string(chars.runes)
}
return string(chars.bytes)
}
func (chars *Chars) ToRunes() []rune {
if chars.runes != nil {
return chars.runes
}
runes := make([]rune, len(chars.bytes))
for idx, b := range chars.bytes {
runes[idx] = rune(b)
}
return runes
}
func (chars *Chars) Slice(b int, e int) Chars {
if chars.runes != nil {
return Chars{runes: chars.runes[b:e]}
}
return Chars{bytes: chars.bytes[b:e]}
}
func (chars *Chars) Split(delimiter string) []Chars {
delim := []rune(delimiter)
numChars := chars.Length()
numDelim := len(delim)
begin := 0
ret := make([]Chars, 0, 1)
for index := 0; index < numChars; {
if index+numDelim <= numChars {
match := true
for off, d := range delim {
if chars.Get(index+off) != d {
match = false
break
}
}
// Found the delimiter
if match {
incr := Max(numDelim, 1)
ret = append(ret, chars.Slice(begin, index+incr))
index += incr
begin = index
continue
}
} else {
// Impossible to find the delimiter in the remaining substring
break
}
index++
}
if begin < numChars || len(ret) == 0 {
ret = append(ret, chars.Slice(begin, numChars))
}
return ret
}

82
src/util/chars_test.go Normal file
View File

@@ -0,0 +1,82 @@
package util
import "testing"
func TestToCharsNil(t *testing.T) {
bs := Chars{bytes: []byte{}}
if bs.bytes == nil || bs.runes != nil {
t.Error()
}
rs := RunesToChars([]rune{})
if rs.bytes != nil || rs.runes == nil {
t.Error()
}
}
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
if chars.ToString() != "foobar" || chars.runes != nil {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
func TestCharsToString(t *testing.T) {
text := "\tabc한글 "
chars := ToChars([]byte(text))
if chars.ToString() != text {
t.Error()
}
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp int) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}
func TestSplit(t *testing.T) {
check := func(str string, delim string, tokens ...string) {
input := ToChars([]byte(str))
result := input.Split(delim)
if len(result) != len(tokens) {
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
str, len(result), len(tokens), result)
}
for idx, token := range tokens {
if result[idx].ToString() != token {
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
str, result[idx].ToString(), token)
}
}
}
check("abc:def::", ":", "abc:", "def:", ":")
check("abc:def::", "-", "abc:def::")
check("abc", "", "a", "b", "c")
check("abc", "a", "a", "bc")
check("abc", "ab", "ab", "c")
check("abc", "abc", "abc")
check("abc", "abcd", "abc")
check("", "abcd", "")
}

View File

@@ -4,21 +4,18 @@ package util
import "C" import "C"
import ( import (
"math"
"os" "os"
"os/exec" "os/exec"
"time" "time"
"unicode/utf8"
) )
// Max returns the largest integer // Max returns the largest integer
func Max(first int, items ...int) int { func Max(first int, second int) int {
max := first if first >= second {
for _, item := range items { return first
if item > max {
max = item
}
} }
return max return second
} }
// Min returns the smallest integer // Min returns the smallest integer
@@ -67,6 +64,15 @@ func Constrain(val int, min int, max int) int {
return val return val
} }
func AsUint16(val int) uint16 {
if val > math.MaxUint16 {
return math.MaxUint16
} else if val < 0 {
return 0
}
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds // DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin( func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration { val time.Duration, min time.Duration, max time.Duration) time.Duration {
@@ -84,58 +90,6 @@ func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
return runes[0 : i+1]
}
// BytesToRunes converts byte array into rune array
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
return runes
}
// TrimLen returns the length of trimmed rune array
func TrimLen(runes []rune) int {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len(runes); j++ {
char := runes[j]
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd { func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")

View File

@@ -3,7 +3,7 @@ package util
import "testing" import "testing"
func TestMax(t *testing.T) { func TestMax(t *testing.T) {
if Max(-2, 5, 1, 4, 3) != 5 { if Max(-2, 5) != 5 {
t.Error("Invalid result") t.Error("Invalid result")
} }
} }
@@ -20,23 +20,3 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3) t.Error("Expected", 3)
} }
} }
func TestTrimLen(t *testing.T) {
check := func(str string, exp int) {
trimmed := TrimLen([]rune(str))
if trimmed != exp {
t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}

View File

@@ -1,5 +1,6 @@
Execute (Setup): Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h') let g:dir = fnamemodify(g:vader_file, ':p:h')
unlet! g:fzf_layout g:fzf_action g:fzf_history_dir
Log 'Test directory: ' . g:dir Log 'Test directory: ' . g:dir
Save &acd Save &acd
@@ -43,6 +44,11 @@ Execute (fzf#run with dir option and noautochdir):
" No change in working directory " No change in working directory
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
AssertEqual cwd, getcwd()
tabclose
AssertEqual cwd, getcwd()
Execute (Incomplete fzf#run with dir option and autochdir): Execute (Incomplete fzf#run with dir option and autochdir):
set acd set acd
let cwd = getcwd() let cwd = getcwd()
@@ -64,6 +70,79 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
Execute (fzf#wrap):
AssertThrows fzf#wrap({'foo': 'bar'})
let opts = fzf#wrap('foobar')
Log opts
AssertEqual '~40%', opts.down
Assert opts.options =~ '--expect='
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {}, 0)
Log opts
AssertEqual '~40%', opts.down
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'down': '50%'})
Log opts
AssertEqual '50%', opts.down
let opts = fzf#wrap('foobar', {'down': '50%'}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'options': '--reverse'})
Log opts
Assert opts.options =~ '--expect='
Assert opts.options =~ '--reverse'
let g:fzf_layout = {'window': 'enew'}
let opts = fzf#wrap('foobar')
Log opts
AssertEqual 'enew', opts.window
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'window')
let opts = fzf#wrap('foobar', {'right': '50%'})
Log opts
Assert !has_key(opts, 'window')
AssertEqual '50%', opts.right
let opts = fzf#wrap('foobar', {'right': '50%'}, 1)
Log opts
Assert !has_key(opts, 'window')
Assert !has_key(opts, 'right')
let g:fzf_action = {'a': 'tabe'}
let opts = fzf#wrap('foobar')
Log opts
Assert opts.options =~ '--expect=a'
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let g:fzf_history_dir = '/tmp'
let opts = fzf#wrap('foobar', {'options': '--color light'})
Log opts
Assert opts.options =~ '--history /tmp/foobar'
Assert opts.options =~ '--color light'
Execute (Cleanup): Execute (Cleanup):
unlet g:dir unlet g:dir
Restore Restore

View File

@@ -36,6 +36,10 @@ end
class Shell class Shell
class << self class << self
def unsets
'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;'
end
def bash def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end end
@@ -45,6 +49,10 @@ class Shell
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end end
def fish
'fish'
end
end end
end end
@@ -57,11 +65,11 @@ class Tmux
@win = @win =
case shell case shell
when :bash when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first
when :zsh when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first
when :fish when :fish
go("new-window -d -P -F '#I' 'fish'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first
else else
raise "Unknown shell: #{shell}" raise "Unknown shell: #{shell}"
end end
@@ -90,6 +98,10 @@ class Tmux
go("send-keys -t #{target} #{args}") go("send-keys -t #{target} #{args}")
end end
def paste str
%x[tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter]
end
def capture pane = 0 def capture pane = 0
File.unlink TEMPNAME while File.exists? TEMPNAME File.unlink TEMPNAME while File.exists? TEMPNAME
wait do wait do
@@ -149,12 +161,6 @@ class TestBase < Minitest::Test
@temp_suffix].join '-' @temp_suffix].join '-'
end end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def readonce def readonce
wait { File.exists?(tempname) } wait { File.exists?(tempname) }
File.read(tempname) File.read(tempname)
@@ -362,7 +368,7 @@ class TestGoFZF < TestBase
end end
def test_query_unicode def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}"
tmux.until { |lines| lines[-2].include? '1/2' } tmux.until { |lines| lines[-2].include? '1/2' }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal ['가나다'], readonce.split($/) assert_equal ['가나다'], readonce.split($/)
@@ -430,6 +436,10 @@ class TestGoFZF < TestBase
test.call 'f3', 'f3' test.call 'f3', 'f3'
test.call 'f2,f4', 'f2', 'f2' test.call 'f2,f4', 'f2', 'f2'
test.call 'f2,f4', 'f4', 'f4' test.call 'f2,f4', 'f4', 'f4'
test.call 'alt-/', [:Escape, :/]
%w[f5 f6 f7 f8 f9 f10].each do |key|
test.call 'f5,f6,f7,f8,f9,f10', key, key
end
test.call '@', '@' test.call '@', '@'
end end
@@ -771,6 +781,13 @@ class TestGoFZF < TestBase
assert_equal %w[4 5 6 9], readonce.split($/) assert_equal %w[4 5 6 9], readonce.split($/)
end end
def test_bind_print_query
tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:print-query'}", :Enter
tmux.until { |lines| lines[-2].end_with? '/1000' }
tmux.send_keys 'print-my-query', 'C-j'
assert_equal %w[print-my-query], readonce.split($/)
end
def test_long_line def test_long_line
data = '.' * 256 * 1024 data = '.' * 256 * 1024
File.open(tempname, 'w') do |f| File.open(tempname, 'w') do |f|
@@ -858,20 +875,34 @@ class TestGoFZF < TestBase
def test_execute def test_execute
output = '/tmp/fzf-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}\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter wait = lambda { |exp| tmux.until { |lines| lines[-2].include? exp } }
tmux.until { |lines| lines[-2].include? '100/100' } writelines tempname, %w[foo'bar foo"bar foo$bar]
tmux.send_keys :Escape, :a, :Escape, :a tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter
wait['3/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Up tmux.send_keys :Up
tmux.send_keys :Escape, :b, :Escape, :b tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Up tmux.send_keys :Up
tmux.send_keys :C tmux.send_keys :C
tmux.send_keys 'foobar' wait['3/3']
tmux.until { |lines| lines[-2].include? '0/100' } tmux.send_keys 'barfoo'
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c wait['0/3']
tmux.send_keys :Escape, :a
wait['/3']
tmux.send_keys :Escape, :b
wait['/3']
tmux.send_keys :Enter tmux.send_keys :Enter
readonce readonce
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'], assert_equal %w[[foo'bar] [foo'bar]
/foo"barfoo"bar/ /foo"barfoo"bar/
/foo$barfoo$barfoo$bar/],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
@@ -879,21 +910,24 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output}; sync)\\"] opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' }
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' } tmux.until { |lines| lines[-2].include? '/4' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare tmux.prepare
readonce readonce
assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], assert_equal [%[foo'bar/foo'bar],
%[foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar],
%[foo'bar foo"bar foobar/foo'bar foo"bar foobar]],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
@@ -912,7 +946,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '1/1' } tmux.until { |lines| lines[-2].include? '1/1' }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
tmux.prepare tmux.prepare
assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) assert_equal ["-c / 'foo'bar"], File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
end end
@@ -1026,7 +1060,7 @@ class TestGoFZF < TestBase
end end
end end
def test_canel def test_cancel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') } tmux.until { |lines| lines[-2].include?('10/10') }
tmux.send_keys '123' tmux.send_keys '123'
@@ -1163,6 +1197,71 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_jump
tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump'"}", :Enter
tmux.until { |lines| lines[-2] == ' 1000/1000' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5 5' }
tmux.until { |lines| lines[-8] == ' 6' }
tmux.send_keys '5'
tmux.until { |lines| lines[-7] == '> 5' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-7] == ' >5' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5>5' }
tmux.send_keys '2'
tmux.until { |lines| lines[-4] == '> 2' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-4] == ' >2' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5>5' }
# Press any key other than jump labels to cancel jump
tmux.send_keys '6'
tmux.until { |lines| lines[-3] == '> 1' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-3] == '>>1' }
tmux.send_keys :Enter
assert_equal %w[5 2 1], readonce.split($/)
end
def test_jump_accept
tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'"}", :Enter
tmux.until { |lines| lines[-2] == ' 1000/1000' }
tmux.send_keys 'C-j'
tmux.until { |lines| lines[-7] == '5 5' }
tmux.send_keys '3'
assert_equal '3', readonce.chomp
end
def test_preview
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') }
tmux.send_keys '555'
tmux.until { |lines| lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| !lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[1].include?(' {555-555}') }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
tmux.send_keys 'foobar'
tmux.until { |lines| !lines[1].include?('{') }
end
def test_preview_hidden
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys '?'
tmux.until { |lines| lines[-2].include?(' {1-1}') }
tmux.send_keys '555'
tmux.until { |lines| lines[-2].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' }
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -1220,16 +1319,29 @@ module TestShell
def test_ctrl_t_unicode def test_ctrl_t_unicode
FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf-test'
tmux.send_keys 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"', :Enter tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat ', 'C-t', pane: 0 tmux.send_keys 'cat ', 'C-t', pane: 0
tmux.until(1) { |lines| lines.item_count >= 1 } tmux.until(1) { |lines| lines.item_count >= 1 }
tmux.send_keys 'fzf-unicode', pane: 1 tmux.send_keys 'fzf-unicode', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys :BTab, :BTab, pane: 1
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include? 'cat' } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' } tmux.until { |lines| lines[-1].include? 'test1test2' }
end end
@@ -1437,14 +1549,26 @@ module CompletionTest
def test_file_completion_unicode def test_file_completion_unicode
FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf-test'
tmux.send_keys 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"', :Enter tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys :BTab, :BTab, pane: 1
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include? 'cat' } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end