Compare commits

...

199 Commits

Author SHA1 Message Date
Junegunn Choi
62ab8ece5e 0.16.1 2017-01-16 12:27:40 +09:00
Junegunn Choi
8e2e63f9b9 Propertly fill window with background color
Close #805
2017-01-16 12:27:32 +09:00
Junegunn Choi
f96173cbe4 Add -L flag to the default find command
Close #781
2017-01-16 12:01:58 +09:00
Amos Bird
11015df52f Add half-page-{up,down} actions (#784) 2017-01-16 11:58:13 +09:00
Junegunn Choi
05ed57a9f0 Merge pull request #794 from junegunn/devel
0.16.0
2017-01-16 02:43:56 +09:00
Junegunn Choi
4bece04207 0.16.0 2017-01-16 02:39:37 +09:00
Junegunn Choi
ede7bfb901 Optimize LightRenderer for slow terminals 2017-01-16 02:26:36 +09:00
Junegunn Choi
44d3faa048 [completion] Restore --height option for kill completion 2017-01-15 22:02:04 +09:00
Junegunn Choi
e0036b5ad2 Add --filepath-word option
Close #802
2017-01-15 19:42:28 +09:00
Junegunn Choi
208d4f2173 [shell] Make layout configurable via $FZF_DEFAULT_OPTS and $FZF_{KEY}_OPTS 2017-01-15 16:15:51 +09:00
Junegunn Choi
dc3957ce79 [completion] Add preview window to kill completion 2017-01-15 15:06:37 +09:00
Junegunn Choi
4ecb7f3a16 Replace --normalize with --literal and enable normalization by default
Ref #790
2017-01-15 13:22:09 +09:00
Junegunn Choi
03f5ef08c8 Use crypto/ssh/terminal instead of external stty command 2017-01-15 13:10:59 +09:00
Junegunn Choi
2720816266 [vim] Use /dev/tty as STDIN when using --height w/o explicit source 2017-01-15 04:33:03 +09:00
Justin M. Keyes
1896aa1748 s:common_sink(): Avoid duplicate BufEnter. (#803)
Later versions of Vim/Nvim handle `:edit <dir>` inside try-catch.

e13b9afe12
https://github.com/vim/vim/pull/1375
2017-01-14 20:55:30 +09:00
Junegunn Choi
5b68027bee Fix $FZF_COMPLETION_OPTS evaluation
Close #799
2017-01-14 01:10:34 +09:00
Junegunn Choi
48863ac55c Update invalid $TERM test case 2017-01-14 01:04:17 +09:00
Junegunn Choi
d64828ce6d Print error message to stderr on unexpected exit 2017-01-11 23:01:56 +09:00
Junegunn Choi
2aa739be81 Fix bug where occurrence of the pattern in header lines are highlighted 2017-01-11 22:47:26 +09:00
Junegunn Choi
9977a3e9fc Make preview renderer suspend early on line wrap 2017-01-11 22:13:40 +09:00
Junegunn Choi
f8082bc53a No need to use /bin/sh to execute stty and tput 2017-01-11 21:48:36 +09:00
Junegunn Choi
996dcb14a3 Make fzf immediately quit when failed to read /dev/tty
Close #798
2017-01-11 02:12:32 +09:00
Junegunn Choi
0c127cfdc1 No need to query row position of the cursor if mouse is disabled 2017-01-10 22:55:55 +09:00
Junegunn Choi
ae274158de Add experimental support for 24-bit colors 2017-01-10 02:16:12 +09:00
Junegunn Choi
340af463cd Add --min-height option for percent --height 2017-01-10 01:04:36 +09:00
Junegunn Choi
78a3f81972 Do not use \e[s and \e[u
Excerpt from http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html:

> - Save cursor position:
>   \033[s
> - Restore cursor position:
>   \033[u
>
> The latter two codes are NOT honoured by many terminal emulators. The
> only ones that I'm aware of that do are xterm and nxterm - even though
> the majority of terminal emulators are based on xterm code. As far as
> I can tell, rxvt, kvt, xiterm, and Eterm do not support them. They are
> supported on the console.

They are also unsupported by Neovim terminal.
2017-01-09 19:09:30 +09:00
Junegunn Choi
d18b8e0d2c Retry flaky test cases 2017-01-09 13:22:24 +09:00
Junegunn Choi
6c6c0a4778 Make util.RuneWidth return 1 for non-displayable characters
Fix line wrapping in preview window
2017-01-09 10:49:05 +09:00
Junegunn Choi
a16d8f66a9 Normalize pattern string before passing it to Algo function 2017-01-09 09:52:17 +09:00
Junegunn Choi
45793d75c2 Add --normalize option to normalize latin script characters
Close #790
2017-01-09 03:12:23 +09:00
Junegunn Choi
9d545f9578 Fix update of multi-select pointer 2017-01-08 02:29:31 +09:00
Junegunn Choi
a30999a785 Prepare for 0.16.0 release 2017-01-08 02:09:56 +09:00
Junegunn Choi
1a50f1eca1 [vim] Use --height instead of fzf-tmux 2017-01-08 02:09:56 +09:00
Junegunn Choi
1448d631a7 Add --height option 2017-01-08 02:09:56 +09:00
Junegunn Choi
fd137a9e87 [bash/zsh-completion] Filter ~/.ssh/known_hosts
Close #791
2017-01-07 22:46:34 +09:00
Jan Edmund Lazo
3670273719 [vim] Use cmd.exe directly on GVim (launcher='%s') (#787) 2017-01-04 14:07:01 +09:00
Jan Edmund Lazo
6c0fd7f9ca [vim] FZF command to handle Windows paths with spaces
- Use noshellslash for strict path expansion in fzf#run and s:cmd
  (shellescape depends on shellslash)
- Double-quote the fzf command for cmd.exe
- Add fzf#shellescape to encapsulate the logic
- Close #786
2017-01-02 02:16:25 +09:00
Jan Edmund Lazo
42a2371d26 [vim] Use cmd.exe in Windows (#785) 2017-01-01 11:48:15 +09:00
Junegunn Choi
45faad7e04 [bash] Addendum fix for #580 2017-01-01 02:23:20 +09:00
Junegunn Choi
73eacf1137 [bash-completion] Always backup existing completion definitions
_fzf_completion_loaded is no longer checked. This change increases the
load time by a few milliseconds, but I can't think of a better way to
handle the issue.

Close #783.
2016-12-31 00:30:00 +09:00
Junegunn Choi
7b0d9e1e07 Apply --tabstop to preview window 2016-12-27 01:35:09 +09:00
Pierre Neidhardt
c7b0764002 [shell] Use '-mindepth 1' to omit root folder in 'find' output (#779)
This removes the need for the 'sed' call. Faster, cleaner.
2016-12-24 12:53:07 +09:00
Daniel Hahler
847c512539 s:execute_term: switch_back: check that self.pbuf exists (#776)
With a `bufhidden=wipe` buffer (e.g. vim-startify) the buffer would not
exist anymore, resulting in an error.
2016-12-19 02:51:19 +09:00
Junegunn Choi
97330ee8fc No need to set MANPATH
Close #774
2016-12-17 11:20:28 +09:00
Pierre Neidhardt
0508e70f9b Overhaul fish functions (#759)
Replace the "temp file" workaround with the "read" function: it's
simpler and faster.

Use proper escaping, remove the custom function.

The "file" widget uses last token as root for the "find" command.
This replaces the equivalent of '**' completion in bash/zsh.
The "$dir" non-expanded variable can be used in FZF_CTRL_T_COMMAND to
set the root.
2016-12-14 15:37:27 +09:00
Marco Hinz
8a502af4c1 Neovim: event handlers always expect three arguments (#768) 2016-12-14 01:56:53 +09:00
Junegunn Choi
c60bfb2b0f [neovim] Keep alternate file unchanged
Close https://github.com/junegunn/fzf.vim/issues/265
2016-12-11 22:32:59 +09:00
Junegunn Choi
16b5902aa2 Fix Linux build (#756) 2016-12-05 02:27:38 +09:00
Junegunn Choi
a442fe0fd0 Truncate long lines in preview window
Add `:wrap` to --preview-window to wrap lines instead

Close #756
2016-12-05 02:13:59 +09:00
Junegunn Choi
ab9ae4f643 [vim] Fix path display in FZF when cwd is ~ 2016-12-03 01:13:56 +09:00
Junegunn Choi
d9a51030ea [vim] Display relative path in prompt 2016-12-02 21:07:23 +09:00
Junegunn Choi
67026718c1 Add BUILD.md 2016-11-27 15:16:53 +09:00
Junegunn Choi
a71c471405 0.15.9 2016-11-26 12:36:24 +09:00
Junegunn Choi
3858086047 Always print scroll indicator in preview window 2016-11-26 12:34:16 +09:00
Junegunn Choi
dffef3d9f3 Update build instructions for ncurses 6 and tcell
Close #357
Close #738
2016-11-26 11:41:57 +09:00
Junegunn Choi
de1c6b8727 [tcell] 24-bit color support
TAGS=tcell make install

    printf "\x1b[38;2;100;200;250mTRUECOLOR\x1b[m\n" |
        TERM=xterm-truecolor fzf --ansi
2016-11-26 00:36:38 +09:00
Junegunn Choi
6f17f412ba Workaround for rendering glitch in case of short-lived input process
: | fzf --preview 'echo foo'
2016-11-25 14:05:37 +09:00
Junegunn Choi
746961bf43 [ncurses6] Suppress tui.Italic on ncurses 5 2016-11-24 13:42:14 +09:00
Junegunn Choi
182a6d99fd [ncurses6] Support italics 2016-11-24 00:13:10 +09:00
Junegunn Choi
af31088481 [ncurses6] Use wcolor_set to support more than 256 color pairs
To build fzf with ncurses 6 on macOS:

    brew install homebrew/dupes/ncurses
    LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
2016-11-24 00:12:43 +09:00
Junegunn Choi
43425158f4 Make escape delay configurable via ncurses standard $ESCDELAY
Also reduce the default delay to 50ms. We should not set it to 0ms as it
breaks escape sequences on WSL. If 50ms is not enough, one can increase
the delay by setting $ESCDELAY to a larger value.
2016-11-23 02:28:03 +09:00
Junegunn Choi
8524ea7441 Do not ignore resize event from ncurses and tcell 2016-11-23 01:58:46 +09:00
Junegunn Choi
6a65006f55 0.15.8 2016-11-19 23:13:26 +09:00
Junegunn Choi
d75ed841a9 Fix --no-bold on --no-color 2016-11-19 23:12:28 +09:00
Junegunn Choi
3cd2547e91 Reduce ESC delay to 100ms 2016-11-19 23:03:27 +09:00
Junegunn Choi
8c661d4e8c Revamp escape sequence processing for WSL
Also add support for alt-[0-9] and f1[12]
2016-11-19 22:42:15 +09:00
Junegunn Choi
4b332d831e Add --no-bold option 2016-11-15 23:57:32 +09:00
Junegunn Choi
22487810ba Update README: link to wiki page 2016-11-15 23:44:04 +09:00
Junegunn Choi
c49e65d926 [shell] Fix pruning condition of find command for CTRL-T and ALT-C
`-fstype dev` is invalid. It's devfs on macOS and devtmpfs on Linux.
2016-11-15 01:52:54 +09:00
Junegunn Choi
2e8814bb57 Add WSL to .github/ISSUE_TEMPLATE.md 2016-11-14 12:26:46 +09:00
Junegunn Choi
dc557c0d4c Update ANSI processor to handle more VT-100 escape sequences
The updated regular expression should include not all but most of the
frequently used ANSI sequences. Close #735.
2016-11-14 02:15:23 +09:00
Junegunn Choi
a2beb159f1 0.15.7 2016-11-09 12:41:46 +09:00
Junegunn Choi
7ce427ff47 Fix panic when color is disabled and header lines contain ANSI colors
Close #732
2016-11-09 12:05:45 +09:00
Junegunn Choi
a221c672fb 0.15.6 2016-11-09 01:45:27 +09:00
Junegunn Choi
f87d382ec8 Fix --color=bw on tcell build 2016-11-09 01:45:06 +09:00
Junegunn Choi
3dfc020fac Merge pull request #730 from laur89/master
Minor README markup fix
2016-11-09 00:06:42 +09:00
Laur Aliste
2d87896939 Minor README markup fix. 2016-11-08 15:41:46 +01:00
Junegunn Choi
2192d8d816 GOOS=windows make release 2016-11-08 03:32:41 +09:00
Junegunn Choi
d206949f62 Wait for additional keys after ESC for up to 100ms
Close #661
2016-11-08 03:07:26 +09:00
Junegunn Choi
4accc69022 Fix flaky test cases 2016-11-08 02:19:05 +09:00
Junegunn Choi
898d8d94c8 Fix issues in tcell renderer and Windows build
- Fix display of CJK wide characters
- Fix horizontal offset of header lines
- Add support for keys with ALT modifier, shift-tab, page-up and down
- Fix util.ExecCommand to properly parse command-line arguments
- Fix redraw on resize
- Implement Pause/Resume for execute action
- Remove runtime check of GOOS
- Change exit status to 2 when tcell failed to start
- TBD: Travis CI build for tcell renderer
    - Pending. tcell cannot reliably ingest keys from tmux send-keys
2016-11-08 02:06:34 +09:00
Michael Kelley
26895da969 Implement tcell-based renderer 2016-11-07 02:32:14 +09:00
Junegunn Choi
0c573b3dff Prepare for termbox/windows build
`TAGS=termbox make` (or `go build -tags termbox`)
2016-11-07 02:32:14 +09:00
Junegunn Choi
2cff00dce2 man fzf in README
Close #726
2016-11-01 00:39:02 +09:00
Junegunn Choi
06a6ad8bca Update ANSI processor to ignore ^N and ^O
This reverts commit 02c6ad0e59.
2016-10-30 12:29:29 +09:00
Junegunn Choi
02c6ad0e59 Strip ^N and ^O from preview output
https://github.com/junegunn/fzf/issues/391#issuecomment-257090266

e.g. fzf --preview 'printf "$(tput setaf 2)foo$(tput sgr0)bar\nbar\n"'
2016-10-30 11:43:06 +09:00
Junegunn Choi
9f321cbe13 Fix header lines being cleared on toggle-preview
Close #722
2016-10-28 03:13:50 +09:00
Junegunn Choi
9f30ca2923 0.15.5 2016-10-23 22:00:32 +09:00
Junegunn Choi
37f2d8f795 [vim] Respect g:fzf_colors
Close #711
2016-10-22 01:14:16 +09:00
Junegunn Choi
400e443a0a Make test cases less susceptible to timeout errors 2016-10-22 00:01:21 +09:00
Junegunn Choi
0a8d2996dc Set foreground color without affecting background
Close #712
2016-10-21 19:35:59 +09:00
Junegunn Choi
cfdb00b971 Allow other options to follow --color without spec 2016-10-21 19:20:16 +09:00
Junegunn Choi
9b9ad39143 [vim] Set g:loaded_fzf 2016-10-18 15:00:47 +09:00
Junegunn Choi
0541c0dbcf Use relative position instead of absolute distance for --tiebreak=end
Fix unintuitive result where `*fzf*/install` is ranked higher than
`fzf/src/fzf/*fzf*-linux_386` on --tiebreak=end.
2016-10-18 01:13:57 +09:00
Junegunn Choi
47b11cb8b4 Merge pull request #701 from nthapaliya/zsh_script_improvements
[zsh] GNU coreutils compatibility
2016-10-14 10:00:58 +09:00
Niraj Thapaliya
d3da310b92 Use command to ignore shell function 2016-10-13 09:53:24 -06:00
Niraj Thapaliya
93e0a6a9de Gnu [ evaluates both sides of a -o condition regardless
It doesn't short circuit like we expect, causing trouble when $dir is
empty

Use shell builtin instead
2016-10-13 09:52:49 -06:00
Junegunn Choi
ac549a853a [fzf-tmux] Fix bash condition
Fix #702
2016-10-13 10:42:26 +09:00
Junegunn Choi
053af9a1c8 [fzf-tmux/vim/nvim] Do not split small window
Close #699
2016-10-12 23:10:21 +09:00
Junegunn Choi
60112def02 Merge pull request #698 from Ambrevar/master
[fish] Yank commandline in fzf-history-widget
2016-10-12 01:54:51 +09:00
Pierre Neidhardt
2134c0c8a9 key-bindings.fish: Yank commandline in fzf-history-widget 2016-10-11 21:15:00 +05:30
Junegunn Choi
3222d62ddf 0.15.4 2016-10-04 02:17:36 +09:00
Junegunn Choi
aeb957a285 Use exact match by default for inverse search term
This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.

| Token    | Match type                 | Description                       |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match                | Items that match `sbtrkt`         |
| `^music` | prefix-exact-match         | Items that start with `music`     |
| `.mp3$`  | suffix-exact-match         | Items that end with `.mp3`        |
| `'wild`  | exact-match (quoted)       | Items that include `wild`         |
| `!fire`  | inverse-exact-match        | Items that do not include `fire`  |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
2016-10-04 02:09:03 +09:00
Junegunn Choi
154cf22ffa Display scroll indicator in preview window 2016-10-04 01:40:45 +09:00
Junegunn Choi
51f532697e Adjust maximum scroll offset
It was possible that a few lines at the bottom may not be visible if
there are lines above that span multiple lines.
2016-10-04 01:39:48 +09:00
Junegunn Choi
01b88539ba [vim] Apply --multi and --prompt to :FZF command 2016-10-04 00:30:04 +09:00
Junegunn Choi
3066b206af Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
2016-10-03 14:33:28 +09:00
Junegunn Choi
04492bab10 Use unicode.IsSpace to cover more whitespace characters 2016-09-29 22:40:22 +09:00
Junegunn Choi
8b0d0342d4 0.15.3 2016-09-29 03:05:20 +09:00
Junegunn Choi
957c12e7d7 Fix SEGV when trying to render preview but the window is closed
Close #677
2016-09-29 02:53:05 +09:00
Junegunn Choi
3b5ae0f8a2 Fix failing unit tests on ANSI attributes 2016-09-29 01:06:47 +09:00
Junegunn Choi
1fc5659842 Add support for more ANSI color attributes (#674)
Dim, underline, blink, reverse
2016-09-29 00:54:27 +09:00
Junegunn Choi
1acd2adce2 Update man page: missing actions 2016-09-26 15:33:46 +09:00
Junegunn Choi
1bc223d4b3 0.15.2 2016-09-25 22:20:43 +09:00
Junegunn Choi
bef405bfa5 Ignore VT100-related escape codes 2016-09-25 19:03:08 +09:00
Junegunn Choi
0612074abe Support high intensity colors
Close #671
2016-09-25 18:11:35 +09:00
Junegunn Choi
3bf51d8362 Merge pull request #670 from maverickwoo/fix-668
[bash-completion] Fix #668
2016-09-25 05:15:24 +09:00
Maverick Woo
2c8479a7c5 Fix #668
Handle uppercase letters in program names. This also deals with `-` and
`.`, both of which are quite common in program names, e.g., `xdg-open`
and `foo.sh`.
2016-09-24 15:39:13 -04:00
Junegunn Choi
8c8b5b313e Add preview-page-up and preview-page-down actions 2016-09-25 04:12:44 +09:00
Junegunn Choi
66d55fd893 Make preview windows scrollable
Close #669

You can use your mouse or binadble preview-up and preview-down actions
to scroll the content of the preview window.

    fzf --preview 'highlight -O ansi {}' --bind alt-j:preview-down,alt-k:preview-up
2016-09-25 02:02:00 +09:00
Junegunn Choi
7fa5e6c861 0.15.1 2016-09-21 01:28:24 +09:00
Junegunn Choi
00f96aae76 Avoid rendering delay when displaying extremely long lines
Related #666
2016-09-21 01:23:41 +09:00
Junegunn Choi
a749e6bd16 Fix temp directory in a test case 2016-09-21 01:15:35 +09:00
Junegunn Choi
791076d366 Fix panic when pattern occurs after 2^15-th column
Fix #666
2016-09-21 01:15:06 +09:00
Junegunn Choi
37f43fbb35 Add --print0 option
Related: #660
2016-09-19 01:15:38 +09:00
Junegunn Choi
401a5fd5ff Printable character in --expect set should not affect --print-query 2016-09-18 14:34:50 +09:00
Junegunn Choi
1854922f0c Truncate the query string if it's too long
Use hard-coded limit to keep it simple. An alternative is to dynamically
calculate the width of the visible area and use it as the limit, but it
can cause unwanted truncation of the query on screen resize/split.
2016-09-18 14:34:48 +09:00
Junegunn Choi
2fc7c18747 Revise ranking algorithm 2016-09-18 14:34:46 +09:00
Junegunn Choi
8ef2420677 Update README 2016-09-13 04:12:03 +09:00
Junegunn Choi
cf6f4d74c4 Merge pull request #657 from ishanray/patch-1
Fix typo in comment
2016-09-11 12:13:40 +09:00
ishanray
f44d40f6b4 Update algo.go 2016-09-10 23:40:55 +04:00
Junegunn Choi
1c81a58127 Merge pull request #654 from qiemem/fix-tmux-groups-dont-break-sockets
[fzf-tmux] Make fzf target correct session in group
2016-09-07 21:36:32 +09:00
Bryan Head
9baf7c4874 Make fzf target correct session in group
Fixes #643
Doesn't break #648
2016-09-06 13:03:07 -05:00
Junegunn Choi
22b089e47e Revert "Unset TMUX before splitting window" (#648)
This reverts commit 4d4447779f.
2016-08-31 14:20:29 +09:00
Junegunn Choi
b166f18220 Merge pull request #646 from qiemem/fix-tmux-groups
[fzf-tmux] Fix grouped tmux session confusion
2016-08-29 12:47:43 +09:00
Junegunn Choi
68600f6ecf Merge pull request #645 from ckafi/split-without-IFS
[zsh-completion] Split default zsh binding at the correct place
2016-08-29 12:47:14 +09:00
Bryan Head
4d4447779f Unset TMUX before splitting window
Avoids confusing grouped sessions.
Fixes #643
2016-08-28 16:57:38 -05:00
Tobias Frilling
639de4c27b Split default zsh binding at the correct place
The command substitution and following word splitting to determine the default
zle widget for ^I formerly only works if the IFS parameter contains a space. Now
it specifically splits at spaces, regardless of IFS.
2016-08-28 20:34:36 +02:00
Junegunn Choi
d87390934e [neovim] Do not resize if the size of the screen has changed
Related #642
2016-08-28 19:27:18 +09:00
Junegunn Choi
411ec2e557 Merge branch 'joshuarubin-master' 2016-08-28 19:18:13 +09:00
Joshua Rubin
f025602841 [vim] Reset window sizes on close
Fix #520
Fix junegunn/fzf.vim#42
2016-08-28 19:17:24 +09:00
Junegunn Choi
f958c9daf5 [vim] Tilde prefix is not allowed for left or right layout 2016-08-24 01:15:35 +09:00
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
66 changed files with 7180 additions and 2857 deletions

View File

@@ -11,6 +11,7 @@
- [ ] Linux - [ ] Linux
- [ ] Mac OS X - [ ] Mac OS X
- [ ] Windows - [ ] Windows
- [ ] Windows Subsystem for Linux
- [ ] Etc. - [ ] Etc.
- Shell - Shell
- [ ] bash - [ ] bash

View File

@@ -1,6 +1,10 @@
language: ruby language: ruby
rvm: matrix:
- 2.2.0 include:
- env: TAGS=
rvm: 2.3.3
# - env: TAGS=tcell
# rvm: 2.2.0
install: install:
- sudo apt-get update - sudo apt-get update

106
BUILD.md Normal file
View File

@@ -0,0 +1,106 @@
Building fzf
============
Build instructions
------------------
### Prerequisites
- `go` executable in $PATH
### Using Makefile
Makefile will set up and use its own `$GOPATH` under the project root.
```sh
# Source files are located in src directory
cd src
# Build fzf binary for your platform in src/fzf
make
# Build fzf binary and copy it to bin directory
make install
# Build 32-bit and 64-bit executables and tarballs
make release
# Build executables and tarballs for Linux using Docker
make linux
```
### Using `go get`
Alternatively, you can build fzf directly with `go get` command without
cloning the repository.
```sh
go get -u github.com/junegunn/fzf/src/fzf
```
Build options
-------------
### With ncurses 6
The official binaries of fzf are built with ncurses 5 because it's widely
supported by different platforms. However ncurses 5 is old and has a number of
limitations.
1. Does not support more than 256 color pairs (See [357][357])
2. Does not support italics
3. Does not support 24-bit color
[357]: https://github.com/junegunn/fzf/issues/357
But you can manually build fzf with ncurses 6 to overcome some of these
limitations. ncurses 6 supports up to 32767 color pairs (1), and supports
italics (2). To build fzf with ncurses 6, you have to install it first. On
macOS, you can use Homebrew to install it.
```sh
brew install homebrew/dupes/ncurses
LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
```
### With tcell
[tcell][tcell] is a portable alternative to ncurses and we currently use it to
build Windows binaries. tcell has many benefits but most importantly, it
supports 24-bit colors. To build fzf with tcell:
```sh
TAGS=tcell make install
```
However, note that tcell has its own issues.
- Poor rendering performance compared to ncurses
- Does not support bracketed-paste mode
- Does not support italics unlike ncurses 6
- Some wide characters are not correctly displayed
Third-party libraries used
--------------------------
- [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
- Licensed under [MIT](http://mattn.mit-license.org)
- [tcell](https://github.com/gdamore/tcell)
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
License
-------
[MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/
[req]: http://golang.org/doc/install
[tcell]: https://github.com/gdamore/tcell

View File

@@ -1,6 +1,130 @@
CHANGELOG CHANGELOG
========= =========
0.16.1
------
- Fixed `--height` option to properly fill the window with the background
color
- Added `half-page-up` and `half-page-down` actions
- Added `-L` flag to the default find command
0.16.0
------
- *Added `--height HEIGHT[%]` option*
- fzf can now display finder without occupying the full screen
- Preview window will truncate long lines by default. Line wrap can be enabled
by `:wrap` flag in `--preview-window`.
- Latin script letters will be normalized before matching so that it's easier
to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`.
- Normalization can be disabled via `--literal`
- Added `--filepath-word` to make word-wise movements/actions (`alt-b`,
`alt-f`, `alt-bs`, `alt-d`) respect path separators
0.15.9
------
- Fixed rendering glitches introduced in 0.15.8
- The default escape delay is reduced to 50ms and is configurable via
`$ESCDELAY`
- Scroll indicator at the top-right corner of the preview window is always
displayed when there's overflow
- Can now be built with ncurses 6 or tcell to support extra features
- *ncurses 6*
- Supports more than 256 color pairs
- Supports italics
- *tcell*
- 24-bit color support
- See https://github.com/junegunn/fzf/blob/master/BUILD.md
0.15.8
------
- Updated ANSI processor to handle more VT-100 escape sequences
- Added `--no-bold` (and `--bold`) option
- Improved escape sequence processing for WSL
- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect`
0.15.7
------
- Fixed panic when color is disabled and header lines contain ANSI colors
0.15.6
------
- Windows binaries! (@kelleyma49)
- Fixed the bug where header lines are cleared when preview window is toggled
- Fixed not to display ^N and ^O on screen
- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by
making fzf wait for additional keystrokes after ESC for up to 100ms
0.15.5
------
- Setting foreground color will no longer set background color to black
- e.g. `fzf --color fg:153`
- `--tiebreak=end` will consider relative position instead of absolute distance
- Updated `fzf#wrap` function to respect `g:fzf_colors`
0.15.4
------
- Added support for range expression in preview and execute action
- e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1`
- `{q}` will be replaced to the single-quoted string of the current query
- Fixed to properly handle unicode whitespace characters
- Display scroll indicator in preview window
- Inverse search term will use exact matcher by default
- This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.
0.15.3
------
- Added support for more ANSI attributes: dim, underline, blink, and reverse
- Fixed race condition in `toggle-preview`
0.15.2
------
- Preview window is now scrollable
- With mouse scroll or with bindable actions
- `preview-up`
- `preview-down`
- `preview-page-up`
- `preview-page-down`
- Updated ANSI processor to support high intensity colors and ignore
some VT100-related escape sequences
0.15.1
------
- Fixed panic when the pattern occurs after 2^15-th column
- Fixed rendering delay when displaying extremely long lines
0.15.0
------
- Improved fuzzy search algorithm
- Added `--algo=[v1|v2]` option so one can still choose the old algorithm
which values the search performance over the quality of the result
- Advanced scoring criteria
- `--read0` to read input delimited by ASCII NUL character
- `--print0` to print output delimited by ASCII NUL character
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 0.13.1
------ ------
- Fixed UI issue with large `--preview` output with many ANSI codes - Fixed UI issue with large `--preview` output with many ANSI codes

175
README.md
View File

@@ -10,18 +10,15 @@ Pros
- No dependencies - No dependencies
- Blazingly fast - Blazingly fast
- e.g. `locate / | fzf`
- Flexible layout
- Runs in fullscreen or in horizontal/vertical split using tmux
- The most comprehensive feature set - The most comprehensive feature set
- Try `fzf --help` and be surprised - Flexible layout using tmux panes
- Batteries included - Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion - Vim/Neovim plugin, key bindings and fuzzy auto-completion
Installation Installation
------------ ------------
fzf project consists of the following: fzf project consists of the following components:
- `fzf` executable - `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane - `fzf-tmux` script for launching fzf in a tmux pane
@@ -30,12 +27,12 @@ fzf project consists of the following:
- Fuzzy auto-completion (bash, zsh) - Fuzzy auto-completion (bash, zsh)
- Vim/Neovim plugin - Vim/Neovim plugin
You can [download fzf executable][bin] alone, but it's recommended that you You can [download fzf executable][bin] alone if you don't need the extra
install the extra stuff using the attached install script. stuff.
[bin]: https://github.com/junegunn/fzf-bin/releases [bin]: https://github.com/junegunn/fzf-bin/releases
#### Using git (recommended) ### Using git
Clone this repository and run Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
@@ -45,7 +42,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
#### Using Homebrew ### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
@@ -56,31 +53,49 @@ brew install fzf
/usr/local/opt/fzf/install /usr/local/opt/fzf/install
``` ```
#### Install as Vim plugin ### Vim plugin
Once you have cloned the repository, add the following line to your .vimrc. You can manually add the directory to `&runtimepath` as follows,
```vim ```vim
" If installed using git
set rtp+=~/.fzf set rtp+=~/.fzf
" If installed using Homebrew
set rtp+=/usr/local/opt/fzf
``` ```
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf But it's recommended that you use a plugin manager like
(recommended): [vim-plug](https://github.com/junegunn/vim-plug).
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
``` ```
#### Upgrading fzf ### Upgrading fzf
fzf is being actively developed and you might want to upgrade it once in a fzf is being actively developed and you might want to upgrade it once in a
while. Please follow the instruction below depending on the installation while. Please follow the instruction below depending on the installation
method. method used.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew update; brew reinstall fzf` - brew: `brew update; brew reinstall fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. However, other
components of the project may not work on Windows. You might want to consider
installing fzf on [Windows Subsystem for Linux][wsl] where everything runs
flawlessly.
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Building fzf
------------
See [BUILD.md](BUILD.md).
Usage Usage
----- -----
@@ -101,27 +116,50 @@ vim $(fzf)
#### Using the finder #### Using the finder
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down - `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items - On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
- Emacs style key bindings - Emacs style key bindings
- Mouse: scroll, click, double-click; shift-click and shift-scroll on - Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode multi-select mode
#### Layout
fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option.
```sh
vim $(fzf --height 40%)
```
Also check out `--reverse` option if you prefer "top-down" layout instead of
the default "bottom-up" layout.
```sh
vim $(fzf --height 40% --reverse)
```
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
default.
```sh
export FZF_DEFAULT_OPTS='--height 40% --reverse'
```
#### Search syntax #### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!rmx` !fire`
| Token | Match type | Description | | Token | Match type | Description |
| -------- | -------------------- | -------------------------------- | | -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` | | `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` | | `'wild` | exact-match (quoted) | Items that include `wild` |
| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` | | `!fire` | inverse-exact-match | Items that do not include `fire` |
| `!'fire` | inverse-exact-match | Items that do not include `fire` | | `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
If you don't prefer fuzzy matching and do not wish to "quote" every word, If you don't prefer fuzzy matching and do not wish to "quote" every word,
start fzf with `-e` or `--exact` option. Note that when `--exact` is set, start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
@@ -144,6 +182,10 @@ or `py`.
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
#### Options
See the man page (`man fzf`) for the full list of options.
Examples Examples
-------- --------
@@ -170,6 +212,13 @@ 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.
Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
fullscreen mode.
```sh
fzf --height 40%
```
Key bindings for command line Key bindings for command line
----------------------------- -----------------------------
@@ -187,14 +236,16 @@ fish.
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options - Set `FZF_ALT_C_OPTS` to pass additional options
If you're on a tmux session, fzf will start in a split pane. You may disable If you're on a tmux session, you can start fzf in a split pane by setting
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the `FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source 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 ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode. mode.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
Fuzzy completion for bash and zsh Fuzzy completion for bash and zsh
--------------------------------- ---------------------------------
@@ -304,7 +355,7 @@ If you have set up fzf for Vim, `:FZF` command will be added.
:FZF ~ :FZF ~
" With options " With options
:FZF --no-sort -m /tmp :FZF --no-sort --reverse --inline-info /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split " Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF! :FZF!
@@ -318,12 +369,12 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for `FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization. customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin
#### `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 |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -342,6 +393,18 @@ 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`, `g:fzf_colors`, 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
---- ----
@@ -379,6 +442,12 @@ fzf
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
If you don't want to exclude hidden files, use the following command:
```sh
export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""'
```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the If you're running fzf in a large git repository, `git ls-tree` can boost up the
@@ -395,19 +464,41 @@ export FZF_DEFAULT_COMMAND='
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means that it doesn't allow reading from STDIN in command substitution, which means
simple `vim (fzf)` won't work as expected. The workaround is to store the result simple `vim (fzf)` won't work as expected. The workaround is to use the `read`
of fzf to a temporary file. fish command:
```sh ```sh
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) fzf | read -l result; and vim $result
``` ```
License or, for multiple results:
-------
[MIT](LICENSE) ```sh
fzf -m | while read -l r; set result $result $r; end; and vim $result
```
Author The globbing system is different in fish and thus `**` completion will not work.
------ However, the `CTRL-T` command will use the last token on the commandline as the
root folder for the recursive search. For instance, hitting `CTRL-T` at the end
of the following commandline
Junegunn Choi ```sh
ls /var/
```
will list all files and folders under `/var/`.
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
make use of this feature. `$dir` defaults to `.` when the last token is not a
valid directory. Example:
```sh
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
```
[License](LICENSE)
------------------
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi

View File

@@ -2,25 +2,52 @@
# 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 [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols)
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 +63,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) max=$columns
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,19 +100,23 @@ 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" -a "$lines" -gt 15 ]; then if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
fzf "${args[@]}" "$fzf" "${args[@]}"
exit $? exit $?
fi fi
# --height option is not allowed
args+=("--no-height")
# Handle zoomed tmux pane by moving it to a temp window # Handle zoomed tmux pane by moving it to a temp window
if tmux list-panes -F '#F' | grep -q Z; then if tmux list-panes -F '#F' | grep -q Z; then
zoomed=1 zoomed=1
@@ -108,7 +137,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 +146,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
@@ -141,16 +163,16 @@ for arg in "${args[@]}"; do
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=$(echo $TMUX | cut -d , -f 1,2) 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=$(echo $TMUX | cut -d , -f 1,2) 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 \
> /dev/null 2>&1 > /dev/null 2>&1

74
install
View File

@@ -2,13 +2,12 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.13.1 pre=1 || version=0.16.1
version=0.13.1 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 +36,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 ;;
@@ -46,7 +46,7 @@ for opt in "$@"; do
--no-update-rc) update_config=0 ;; --no-update-rc) update_config=0 ;;
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin|--pre) ;; --bin) ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
help help
@@ -109,9 +109,17 @@ 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 [[ ! "$version" =~ alpha ]]; then
if [ -x "$fzf_base"/bin/fzf ]; then if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
@@ -127,15 +135,17 @@ download() {
return return
fi fi
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz local url
if command -v curl > /dev/null; then [[ "$version" =~ alpha ]] &&
curl -fL $url | tar -xz url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz ||
elif command -v wget > /dev/null; then url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
wget -O - $url | tar -xz set -o pipefail
else if ! (try_curl $url || try_wget $url); then
binary_error="curl or wget not found" set +o pipefail
binary_error="Failed to download with curl and wget"
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 +168,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,26 +242,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 -u github.com/junegunn/fzf/src/fzf) ... " echo " - $binary_error !!!"
if [ -z "${GOPATH-}" ]; then fi
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" if command -v go > /dev/null; then
mkdir -p "$GOPATH" echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
fi if [ -z "${GOPATH-}" ]; then
if go get -u github.com/junegunn/fzf/src/fzf; then export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
echo "OK" mkdir -p "$GOPATH"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" fi
else if go get -u github.com/junegunn/fzf/src/fzf; then
echo "Failed to build binary ..." echo "OK"
install_ruby_fzf cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
fi
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
@@ -290,12 +302,6 @@ if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin" export PATH="\$PATH:$fzf_base/bin"
fi fi
# Man path
# --------
if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then
export MANPATH="\$MANPATH:$fzf_base/man"
fi
# Auto-completion # Auto-completion
# --------------- # ---------------
$fzf_completion $fzf_completion

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

@@ -0,0 +1,54 @@
.ig
The MIT License (MIT)
Copyright (c) 2017 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 "Jan 2017" "fzf 0.16.1" "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

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jun 2016" "fzf 0.13.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2017" "fzf 0.16.1" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -47,6 +47,19 @@ Case-insensitive match (default: smart-case match)
.TP .TP
.B "+i" .B "+i"
Case-sensitive match Case-sensitive match
.TP
.B "--literal"
Do not normalize latin script letters for matching.
.TP
.BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2)
.br
.BR v2 " Optimal scoring algorithm (quality)"
.br
.BR v1 " Faster but not guaranteed to find the optimal result (performance)"
.br
.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.
@@ -116,10 +129,30 @@ 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 (default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen. on the center of the screen.
.TP .TP
.B "--filepath-word"
Make word-wise movements and actions respect path separators. The following
actions are affected:
\fBbackward-kill-word\fR
.br
\fBbackward-word\fR
.br
\fBforward-word\fR
.br
\fBkill-word\fR
.TP
.BI "--jump-labels=" "CHARS" .BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using
the full screen.
.TP
.BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10).
Ignored when \fB--height\fR is not specified.
.TP
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
@@ -175,7 +208,9 @@ Number of spaces for a tab character (default: 8)
.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
color mappings. Ansi color code of -1 denotes terminal default color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format, but the support for 24-bit colors is experimental and only works when
\fB--height\fR option is used.
.RS .RS
e.g. \fBfzf --color=bg+:24\fR e.g. \fBfzf --color=bg+:24\fR
@@ -206,6 +241,9 @@ e.g. \fBfzf --color=bg+:24\fR
\fBheader \fRHeader \fBheader \fRHeader
.RE .RE
.TP .TP
.B "--no-bold"
Do not use bold text
.TP
.B "--black" .B "--black"
Use black background Use black background
.SS History .SS History
@@ -222,17 +260,24 @@ automatically truncated when the number of the lines exceeds the value.
.TP .TP
.BI "--preview=" "COMMAND" .BI "--preview=" "COMMAND"
Execute the given command for the current line and display the result on the Execute the given command for the current line and display the result on the
preview window. \fB{}\fR is the placeholder for the quoted string of the preview window. \fB{}\fR in the command is the placeholder that is replaced to
current line. the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current
query string.
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" .BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with Determine the layout of the preview window. If the argument ends with
\fB:hidden\fR, the preview window will be hidden by default until \fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag.
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@@ -275,6 +320,12 @@ with the default enter key.
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
.TP .TP
.B "--read0"
Read input delimited by ASCII NUL character instead of newline character
.TP
.B "--print0"
Print output delimited by ASCII NUL character instead of newline character
.TP
.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.
@@ -342,7 +393,7 @@ 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 lines 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. In this case, fzf performs exact match by default.
.SS Exact-match by default .SS Exact-match by default
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
@@ -366,7 +417,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS) .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIf[1-10]\fR \fIalt-[0-9]\fR
\fIf[1-12]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR \fIspace\fR
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
@@ -418,6 +470,12 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview-down\fR
\fBpreview-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBselect-all\fR \fBselect-all\fR
@@ -440,9 +498,11 @@ binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR \fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line. You can use the same placeholder expressions as in \fB--preview\fR.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors. If the command contains parentheses, fzf may fail to parse the expression. In
that case, you can use any of the following alternative notations to avoid
parse errors.
\fBexecute[...]\fR \fBexecute[...]\fR
\fBexecute~...~\fR \fBexecute~...~\fR
@@ -461,7 +521,7 @@ alternative notations to avoid parse errors.
.RS .RS
This is the special form that frees you from parse errors as it does not expect 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 the closing character. The catch is that it should be the last one in the
comma-separated list. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command \fBexecute-multi(...)\fR is an alternative action that executes the command

View File

@@ -21,7 +21,13 @@
" 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%' if exists('g:loaded_fzf')
finish
endif
let g:loaded_fzf = 1
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
@@ -74,7 +80,13 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return escape(a:path, ' $%#''"\') let escaped_chars = '$%#''"'
if has('unix')
let escaped_chars .= ' \'
endif
return escape(a:path, escaped_chars)
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -104,11 +116,154 @@ 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 !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
\ && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:get_color(attr, ...)
for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm')
if code =~ '^[0-9]\+$'
return code
endif
endfor
return ''
endfunction
function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors)
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
" Colors: g:fzf_colors
let opts.options = s:defaults() .' '. get(opts, 'options', '')
" History: g:fzf_history_dir
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#shellescape(path)
if has('win32') || has('win64')
let shellslash = &shellslash
try
set noshellslash
return shellescape(a:path)
finally
let &shellslash = shellslash
endtry
endif
return shellescape(a:path)
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
set shell=sh let useshellslash = &shellslash
if has('nvim') && bufexists('term://*:FZF')
if has('win32') || has('win64')
set shell=cmd.exe
set noshellslash
else
set shell=sh
endif
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 +277,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) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source)
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
@@ -135,23 +292,35 @@ 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') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
let use_height = has_key(dict, 'down') &&
\ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) &&
\ executable('tput') && filereadable('/dev/tty')
let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
let term = has('nvim') && !tmux
if use_height
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
elseif term
let optstr .= ' --no-height'
endif
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') && !tmux if term
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)
call s:popd(dict, ret) \ : s:execute(dict, command, use_height, temps)
return ret call s:callback(dict, lines)
return lines
finally finally
let &shell = oshell let &shell = oshell
let &shellslash = useshellslash
endtry endtry
endfunction endfunction
@@ -170,10 +339,10 @@ function! s:fzf_tmux(dict)
if s:present(a:dict, o) if s:present(a:dict, o)
let spec = a:dict[o] let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~' if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict) let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else else
" Legacy boolean option " Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : spec) let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif endif
break break
endif endif
@@ -183,7 +352,8 @@ function! s:fzf_tmux(dict)
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right') return s:present(a:dict, 'up', 'down') && &lines > 15 ||
\ s:present(a:dict, 'left', 'right') && &columns > 40
endfunction endfunction
function! s:pushd(dict) function! s:pushd(dict)
@@ -200,22 +370,17 @@ function! s:pushd(dict)
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 'lcd' 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()
@@ -228,7 +393,11 @@ function! s:xterm_launcher()
\ &columns, &lines/2, getwinposx(), getwinposy()) \ &columns, &lines/2, getwinposx(), getwinposy())
endfunction endfunction
unlet! s:launcher unlet! s:launcher
let s:launcher = function('s:xterm_launcher') if has('win32') || has('win64')
let s:launcher = '%s'
else
let s:launcher = function('s:xterm_launcher')
endif
function! s:exit_handler(code, command, ...) function! s:exit_handler(code, command, ...)
if a:code == 130 if a:code == 130
@@ -243,20 +412,31 @@ function! s:exit_handler(code, command, ...)
return 1 return 1
endfunction endfunction
function! s:execute(dict, command, temps) abort function! s:execute(dict, command, use_height, temps) abort
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null if has('unix') && !a:use_height
silent! !clear 2> /dev/null
endif
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running') if has('gui_running')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'") if has('unix')
let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'"
endif
let command = printf(fmt, escaped)
else else
let command = escaped let command = escaped
endif endif
execute 'silent !'.command if a:use_height
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else
execute 'silent !'.command
endif
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,15 +447,17 @@ 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)
if a:val =~ '%$' let val = substitute(a:val, '^\~', '', '')
let size = a:max * str2nr(a:val[:-2]) / 100 if val =~ '%$'
let size = a:max * str2nr(val[:-2]) / 100
else else
let size = min([a:max, str2nr(a:val)]) let size = min([a:max, str2nr(val)])
endif endif
let srcsz = -1 let srcsz = -1
@@ -285,6 +467,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
@@ -300,24 +483,25 @@ function! s:split(dict)
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let ppos = s:getpos() let ppos = s:getpos()
try try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val[1:], a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
if s:present(a:dict, 'window') if s:present(a:dict, 'window')
execute a:dict.window execute 'keepalt' a:dict.window
else elseif !s:splittable(a:dict)
execute (tabpagenr()-1).'tabnew' execute (tabpagenr()-1).'tabnew'
else
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val, a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
endif endif
return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }] return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
finally finally
@@ -326,21 +510,24 @@ function! s:split(dict)
endfunction endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd()
let pbuf = bufnr('')
let [ppos, 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(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'name': 'FZF', 'winopts': winopts, 'command': a:command } \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
\ 'columns': &columns, 'command': a:command }
function! fzf.switch_back(inplace) function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode if bufexists(self.pbuf)
" execute "normal! \<c-^>" execute 'keepalt b' self.pbuf
b # endif
" No other listed buffer " No other listed buffer
if bufnr('') == self.buf if bufnr('') == self.buf
enew enew
endif endif
endif endif
endfunction endfunction
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code, _event)
if s:getpos() == self.ppos " {'window': 'enew'} if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
@@ -356,36 +543,71 @@ function! s:execute_term(dict, command, temps) abort
execute self.ppos.win.'wincmd w' execute self.ppos.win.'wincmd w'
endif endif
if bufexists(self.buf)
execute 'bd!' self.buf
endif
if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos
execute self.winrest
endif
if !s:exit_handler(a:code, self.command, 1) 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)
call self.switch_back(s:getpos() == self.ppos)
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
@@ -394,76 +616,45 @@ 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 function! s:shortpath()
if empty(a:lines) let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
return return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/')
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 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 = { 'options': '--multi ' }
let opts = {} if len(args) && isdirectory(expand(args[-1]))
if len(args) > 0 && isdirectory(expand(args[-1])) let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') let opts.options .= ' --prompt '.fzf#shellescape(opts.dir)
else
let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath())
endif endif
if !a:bang let opts.options .= ' '.join(args)
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) call fzf#run(fzf#wrap('FZF', opts, a:bang))
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

@@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.bash # /_/ /___/_/-completion.bash
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@@ -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@^\./@@'
} }
@@ -30,9 +30,17 @@ fi
########################################################### ###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
__fzfcmd_complete() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -43,35 +51,43 @@ _fzf_opts_completion() {
opts=" opts="
-x --extended -x --extended
-e --exact -e --exact
--algo
-i +i -i +i
-n --nth -n --nth
--with-nth
-d --delimiter -d --delimiter
+s --no-sort +s --no-sort
--tac --tac
--tiebreak --tiebreak
--bind
-m --multi -m --multi
--no-mouse --no-mouse
--color --bind
--black --cycle
--reverse
--no-hscroll --no-hscroll
--jump-labels
--height
--literal
--reverse
--margin
--inline-info --inline-info
--prompt --prompt
--header
--header-lines
--ansi
--tabstop
--color
--no-bold
--history
--history-size
--preview
--preview-window
-q --query -q --query
-1 --select-1 -1 --select-1
-0 --exit-0 -0 --exit-0
-f --filter -f --filter
--print-query --print-query
--expect --expect
--toggle-sort --sync"
--sync
--cycle
--history
--history-size
--header
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@@ -108,7 +124,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
@@ -116,8 +132,8 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf local cur base dir leftover matches trigger cmd fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
@@ -132,8 +148,7 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
tput sc matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
printf "%q$3 " "$item" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
@@ -142,7 +157,7 @@ __fzf_generic_path_completion() {
else else
COMPREPLY=( "$cur" ) COMPREPLY=( "$cur" )
fi fi
tput rc printf '\e[5n'
return 0 return 0
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@@ -160,18 +175,17 @@ _fzf_complete() {
local cur selected trigger cmd fzf post local cur selected trigger cmd fzf post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post=cat
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
tput sc selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
tput rc printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -200,10 +214,9 @@ _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf local selected fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
tput sc selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') printf '\e[5n'
tput rc
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=( "$selected" ) COMPREPLY=( "$selected" )
@@ -213,16 +226,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 -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\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
) )
} }
@@ -261,12 +274,9 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.11.3' ]; then eval $(complete |
# Really wish I could use associative array but OSX comes with bash 3.2 :( sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
eval $(complete | \grep '\-F' | \grep -v _fzf_ | _fzf_orig_completion_filter)
\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
fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
@@ -277,7 +287,7 @@ _fzf_defc() {
cmd="$1" cmd="$1"
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_$cmd" orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}" orig="${!orig_var}"
if [ -n "$orig" ]; then if [ -n "$orig" ]; then
printf -v def "$orig" "$func" printf -v def "$orig" "$func"

View File

@@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.zsh # /_/ /___/_/-completion.zsh
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@@ -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@^\./@@'
} }
@@ -30,6 +30,11 @@ fi
########################################################### ###########################################################
__fzfcmd_complete() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches 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"
@@ -39,18 +44,18 @@ __fzf_generic_path_completion() {
fzf_opts=$4 fzf_opts=$4
suffix=$5 suffix=$5
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
setopt localoptions nonomatch setopt localoptions nonomatch
dir="$base" dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then if [[ -z "$dir" || -d ${~dir} ]]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
dir=${~dir} dir=${~dir}
matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
echo -n "${(q)item}$suffix " echo -n "${(q)item}$suffix "
done) done)
matches=${matches% } matches=${matches% }
@@ -58,6 +63,7 @@ __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")
@@ -76,7 +82,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" &
) )
@@ -89,29 +95,30 @@ _fzf_complete() {
post="${funcstack[2]}_post" post="${funcstack[2]}_post"
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
_fzf_feed_fifo "$fifo" _fzf_feed_fifo "$fifo"
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
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 '*') \ command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\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
) )
} }
@@ -136,7 +143,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
@@ -155,12 +162,13 @@ fzf-completion() {
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence) # Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
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})
@@ -183,7 +191,7 @@ fzf-completion() {
[ -z "$fzf_default_completion" ] && { [ -z "$fzf_default_completion" ] && {
binding=$(bindkey '^I') binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(w)2] [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2]
unset binding unset binding
} }

View File

@@ -1,11 +1,11 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-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 | cut -b3-"}"
eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -13,8 +13,13 @@ __fzf_select__() {
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() { __fzfcmd() {
[ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" __fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
__fzf_select_tmux__() { __fzf_select_tmux__() {
@@ -26,7 +31,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") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") 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__ --no-height)\"; 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() {
@@ -41,9 +46,9 @@ fzf-file-widget() {
__fzf_cd__() { __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 'devfs' -o -fstype 'devtmpfs' -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 $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
__fzf_history__() ( __fzf_history__() (
@@ -51,8 +56,8 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
\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
@@ -60,25 +65,18 @@ __fzf_history__() (
fi fi
) )
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
if [[ ! -o vi ]]; 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'
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
if [ $__use_bind_x -eq 1 ]; then if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"' bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then elif __fzf_use_tmux__; then
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else else
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
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
@@ -102,24 +100,22 @@ else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position # - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_bind_x -eq 1 ]; then if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"' bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then elif __fzf_use_tmux__; then
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
else else
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
fi fi
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# 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-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi fi
unset -v __use_tmux __use_bind_x
fi fi

View File

@@ -1,58 +1,75 @@
# Key bindings # Key bindings
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
# Due to a bug of fish, we cannot use command substitution,
# so we use temporary file instead
if [ -z "$TMPDIR" ]
set -g TMPDIR /tmp
end
function __fzf_escape # Store last token in $dir as root for the 'find' command
while read item function fzf-file-widget -d "List files and folders"
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' set -l dir (commandline -t)
# The commandline token might be escaped, we need to unescape it.
set dir (eval "printf '%s' $dir")
if [ ! -d "$dir" ]
set dir .
end end
end # Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
set dir (string replace --regex '(.)/+$' '$1' "$dir")
function fzf-file-widget # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-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 's#^\./##'"
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 set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
return
end
if [ "$dir" != . ]
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- (string escape $i)
commandline -it -- ' '
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function fzf-history-widget function fzf-history-widget -d "Show command history"
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
and commandline (cat $TMPDIR/fzf.result) begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m"
history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function fzf-cd-widget function fzf-cd-widget -d "Change directory"
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -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) set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result" begin
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
and cd (cat $TMPDIR/fzf.result) eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
[ "$result" ]; and cd $result
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function __fzfcmd function __fzfcmd
set -q FZF_TMUX; or set FZF_TMUX 1 set -q FZF_TMUX; or set FZF_TMUX 0
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
if [ $FZF_TMUX -eq 1 ] if [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
echo "fzf-tmux -d40%"
end
else else
echo "fzf" echo "fzf"
end end
@@ -68,4 +85,3 @@ function fzf_key_bindings
bind -M insert \ec fzf-cd-widget bind -M insert \ec fzf-cd-widget
end end
end end

View File

@@ -4,33 +4,48 @@ if [[ $- == *i* ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-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 | cut -b3-"}"
eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do setopt localoptions pipefail 2> /dev/null
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$?
echo echo
return $ret
}
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
} }
__fzfcmd() { __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" __fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
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
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -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 $FZF_ALT_C_OPTS"):-.}" setopt localoptions pipefail 2> /dev/null
cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}"
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,8 +53,10 @@ 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
setopt localoptions noglobsubst 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}") ) selected=( $(fc -l 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) )
local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
@@ -47,6 +64,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

@@ -33,17 +33,24 @@ endif
all: fzf/$(BINARY) all: fzf/$(BINARY)
ifeq ($(GOOS),windows)
release: fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \
rm -f $(RELEASE32).exe $(RELEASE64).exe
else
release: test fzf/$(BINARY32) fzf/$(BINARY64) 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)
endif
$(SRCDIR): $(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR)) mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR) ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES) deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get cd $(SRCDIR) && go get -tags "$(TAGS)"
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
@@ -52,7 +59,7 @@ android-build: $(SRCDIR)
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
test: deps test: deps
SHELL=/bin/sh go test -v ./... SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
@@ -63,10 +70,10 @@ 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 -ldflags -w -tags "$(TAGS)" -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps fzf/$(BINARY64): deps
cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64) cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY) $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR)

View File

@@ -47,66 +47,40 @@ proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new
version was shown to be an order of magnitude faster on certain cases. It also version was shown to be an order of magnitude faster on certain cases. It also
starts much faster though the difference may not be noticeable. starts much faster though the difference may not be noticeable.
Differences with Ruby version
-----------------------------
The Go version is designed to be perfectly compatible with the previous Ruby
version. The only behavioral difference is that the new version ignores the
numeric argument to `--sort=N` option and always sorts the result regardless
of the number of matches. The value was introduced to limit the response time
of the query, but the Go version is blazingly fast (almost instant response
even for 1M+ items) so I decided that it's no longer required.
System requirements
-------------------
Currently, prebuilt binaries are provided only for OS X and Linux. The install
script will fall back to the legacy Ruby version on the other systems, but if
you have Go 1.4 installed, you can try building it yourself.
However, as pointed out in [golang.org/doc/install][req], the Go version may
not run on CentOS/RHEL 5.x, and if that's the case, the install script will
choose the Ruby version instead.
The Go version depends on [ncurses][ncurses] and some Unix system calls, so it
shouldn't run natively on Windows at the moment. But it won't be impossible to
support Windows by falling back to a cross-platform alternative such as
[termbox][termbox] only on Windows. If you're interested in making fzf work on
Windows, please let me know.
Build Build
----- -----
See [BUILD.md](../BUILD.md)
Test
----
Unit tests can be run with `make test`. Integration tests are written in Ruby
script that should be run on tmux.
```sh ```sh
# Build fzf executables and tarballs # Unit tests
make release make test
# Install the executable to ../bin directory # Install the executable to ../bin directory
make install make install
# Build executables and tarballs for Linux using Docker # Integration tests
make linux ruby ../test/test_go.rb
``` ```
Contribution
------------
For the time being, I will not add or accept any new features until we can be
sure that the implementation is stable and we have a sufficient number of test
cases. However, fixes for obvious bugs and new test cases are welcome.
I also care much about the performance of the implementation, so please make
sure that your change does not result in performance regression. And please be
noted that we don't have a quantitative measure of the performance yet.
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [ncurses][ncurses] - [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org/2013) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org/2014) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
- Licensed under [MIT](http://mattn.mit-license.org)
- [tcell](https://github.com/gdamore/tcell)
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
License License
------- -------
@@ -118,4 +92,4 @@ License
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/ [ncurses]: https://www.gnu.org/software/ncurses/
[req]: http://golang.org/doc/install [req]: http://golang.org/doc/install
[termbox]: https://github.com/nsf/termbox-go [tcell]: https://github.com/gdamore/tcell

View File

@@ -1,37 +1,143 @@
package algo package algo
/*
Algorithm
---------
FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given
text in O(n) time where n is the length of the text. Once the position of the
last character is located, it traverses backwards to see if there's a shorter
substring that matches the pattern.
a_____b___abc__ To find "abc"
*-----*-----*> 1. Forward scan
<*** 2. Backward scan
The algorithm is simple and fast, but as it only sees the first occurrence,
it is not guaranteed to find the occurrence with the highest score.
a_____b__c__abc
*-----*--* ***
FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find
the optimal solution (highest score) according to the scoring criteria. Unlike
the original algorithm, omission or mismatch of a character in the pattern is
not allowed.
Performance
-----------
The new V2 algorithm is slower than V1 as it examines all occurrences of the
pattern instead of stopping immediately after finding the first one. The time
complexity of the algorithm is O(nm) if a match is found and O(n) otherwise
where n is the length of the item and m is the length of the pattern. Thus, the
performance overhead may not be noticeable for a query with high selectivity.
However, if the performance is more important than the quality of the result,
you can still choose v1 algorithm with --algo=v1.
Scoring criteria
----------------
- We prefer matches at special positions, such as the start of a word, or
uppercase character in camelCase words.
- That is, we prefer an occurrence of the pattern with more characters
matching at special positions, even if the total match length is longer.
e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff"
````````````
- Also, if the first character in the pattern appears at one of the special
positions, the bonus point for the position is multiplied by a constant
as it is extremely likely that the first character in the typed pattern
has more significance than the rest.
e.g. "fo-bar" vs. "foob-r" on "br"
``````
- But since fzf is still a fuzzy finder, not an acronym finder, we should also
consider the total length of the matched substring. This is why we have the
gap penalty. The gap penalty increases as the length of the gap (distance
between the matching characters) increases, so the effect of the bonus is
eventually cancelled at some point.
e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff"
```````````
- Consequently, it is crucial to find the right balance between the bonus
and the gap penalty. The parameters were chosen that the bonus is cancelled
when the gap size increases beyond 8 characters.
- The bonus mechanism can have the undesirable side effect where consecutive
matches are ranked lower than the ones with gaps.
e.g. "foobar" vs. "foo-bar" on "foob"
```````
- To correct this anomaly, we also give extra bonus point to each character
in a consecutive matching chunk.
e.g. "foobar" vs. "foo-bar" on "foob"
``````
- The amount of consecutive bonus is primarily determined by the bonus of the
first character in the chunk.
e.g. "foobar" vs. "out-of-bound" on "oob"
````````````
*/
import ( import (
"fmt"
"strings" "strings"
"unicode" "unicode"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
/* var DEBUG bool
* String matching algorithms here do not use strings.ToLower to avoid
* performance penalty. And they assume pattern runes are given in lowercase
* letters when caseSensitive is false.
*
* 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 contains the results of running a match function.
type Result struct { type Result struct {
Start int32 // TODO int32 should suffice
End int32 Start int
End int
// Items are basically sorted by the lengths of matched substrings. Score int
// But we slightly adjust the score with bonus for better results.
Bonus int32
} }
const (
scoreMatch = 16
scoreGapStart = -3
scoreGapExtention = -1
// We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over
// shorter fuzzy matches. The bonus point here was specifically chosen that
// the bonus is cancelled when the gap between the acronyms grows over
// 8 characters, which is approximately the average length of the words found
// in web2 dictionary and my file system.
bonusBoundary = scoreMatch / 2
// Although bonus point for non-word characters is non-contextual, we need it
// for computing bonus points for consecutive chunks starting with a non-word
// character.
bonusNonWord = scoreMatch / 2
// Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtention
// Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
// The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is
// still respected.
bonusFirstCharMultiplier = 2
)
type charClass int type charClass int
const ( const (
@@ -42,94 +148,383 @@ const (
charNumber charNumber
) )
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { func posArray(withPos bool, len int) *[]int {
var bonus int32 if withPos {
pidx := 0 pos := make([]int, 0, len)
lenPattern := len(pattern) return &pos
consecutive := false
prevClass := charNonWord
for index := 0; index < eidx; index++ {
char := runes[index]
var class charClass
if unicode.IsLower(char) {
class = charLower
} else if unicode.IsUpper(char) {
class = charUpper
} else if unicode.IsLetter(char) {
class = charLetter
} else if unicode.IsNumber(char) {
class = charNumber
} else {
class = charNonWord
}
var point int32
if prevClass == charNonWord && class != charNonWord {
// Word boundary
point = 2
} else if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
point = 1
}
prevClass = class
if index >= sidx {
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := pattern[pidx]
if pchar == char {
// Boost bonus for the first character in the pattern
if pidx == 0 {
point *= 2
}
// Bonus to consecutive matching chars
if consecutive {
point++
}
bonus += point
if pidx++; pidx == lenPattern {
break
}
consecutive = true
} else {
consecutive = false
}
}
} }
return bonus return nil
} }
// FuzzyMatch performs fuzzy-match func alloc16(offset int, slab *util.Slab, size int, clear bool) (int, []int16) {
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { if slab != nil && cap(slab.I16) > offset+size {
if len(pattern) == 0 { slice := slab.I16[offset : offset+size]
return Result{0, 0, 0} if clear {
for idx := range slice {
slice[idx] = 0
}
}
return offset + size, slice
}
return offset, make([]int16, size)
}
func alloc32(offset int, slab *util.Slab, size int, clear bool) (int, []int32) {
if slab != nil && cap(slab.I32) > offset+size {
slice := slab.I32[offset : offset+size]
if clear {
for idx := range slice {
slice[idx] = 0
}
}
return offset + size, slice
}
return offset, make([]int32, size)
}
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) {
return charLower
} else if unicode.IsUpper(char) {
return charUpper
} else if unicode.IsNumber(char) {
return charNumber
} else if unicode.IsLetter(char) {
return charLetter
}
return charNonWord
}
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return charClassOfAscii(char)
}
return charClassOfNonAscii(char)
}
func bonusFor(prevClass charClass, class charClass) int16 {
if prevClass == charNonWord && class != charNonWord {
// Word boundary
return bonusBoundary
} else if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
} else if class == charNonWord {
return bonusNonWord
}
return 0
}
func bonusAt(input util.Chars, idx int) int16 {
if idx == 0 {
return bonusBoundary
}
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
}
func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 {
return r
}
n := normalized[r]
if n > 0 {
return n
}
return r
}
// Algo functions make two assumptions
// 1. "pattern" is given in lowercase if "caseSensitive" is false
// 2. "pattern" is already normalized if "normalize" is true
type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position.
// If the input string is too long, consider finding the matching chars in
// this phase as well (non-optimal alignment).
N := input.Length()
M := len(pattern)
switch M {
case 0:
return Result{0, 0, 0}, posArray(withPos, M)
case 1:
return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab)
}
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
if slab != nil && N*M > cap(slab.I16) {
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
}
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0
offset32 := 0
// Bonus point for each position
offset16, B := alloc16(offset16, slab, N, false)
// The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M, false)
// Rune array
offset32, T := alloc32(offset32, slab, N, false)
// Phase 1. Check if there's a match and calculate bonus for each point
pidx, lastIdx, prevClass := 0, 0, charNonWord
for idx := 0; idx < N; idx++ {
char := input.Get(idx)
var class charClass
if char <= unicode.MaxASCII {
class = charClassOfAscii(char)
} else {
class = charClassOfNonAscii(char)
}
if !caseSensitive && class == charUpper {
if char <= unicode.MaxASCII {
char += 32
} else {
char = unicode.To(unicode.LowerCase, char)
}
}
if normalize {
char = normalizeRune(char)
}
T[idx] = char
B[idx] = bonusFor(prevClass, class)
prevClass = class
if pidx < M {
if char == pattern[pidx] {
lastIdx = idx
F[pidx] = int32(idx)
pidx++
}
} else {
if char == pattern[M-1] {
lastIdx = idx
}
}
}
if pidx != M {
return Result{-1, -1, 0}, nil
}
// Phase 2. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission.
width := lastIdx - int(F[0]) + 1
offset16, H := alloc16(offset16, slab, width*M, false)
// Possible length of consecutive chunk at each position.
offset16, C := alloc16(offset16, slab, width*M, false)
maxScore, maxScorePos := int16(0), 0
for i := 0; i < M; i++ {
I := i * width
inGap := false
for j := int(F[i]); j <= lastIdx; j++ {
j0 := j - int(F[0])
var s1, s2, consecutive int16
if j > int(F[i]) {
if inGap {
s2 = H[I+j0-1] + scoreGapExtention
} else {
s2 = H[I+j0-1] + scoreGapStart
}
}
if pattern[i] == T[j] {
var diag int16
if i > 0 && j0 > 0 {
diag = H[I-width+j0-1]
}
s1 = diag + scoreMatch
b := B[j]
if i > 0 {
// j > 0 if i > 0
consecutive = C[I-width+j0-1] + 1
// Break consecutive chunk
if b == bonusBoundary {
consecutive = 1
} else if consecutive > 1 {
b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1]))
}
} else {
consecutive = 1
b *= bonusFirstCharMultiplier
}
if s1+b < s2 {
s1 += B[j]
consecutive = 0
} else {
s1 += b
}
}
C[I+j0] = consecutive
inGap = s1 < s2
score := util.Max16(util.Max16(s1, s2), 0)
if i == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, j
}
H[I+j0] = score
}
if DEBUG {
if i == 0 {
fmt.Print(" ")
for j := int(F[i]); j <= lastIdx; j++ {
fmt.Printf(" " + string(input.Get(j)) + " ")
}
fmt.Println()
}
fmt.Print(string(pattern[i]) + " ")
for idx := int(F[0]); idx < int(F[i]); idx++ {
fmt.Print(" 0 ")
}
for idx := int(F[i]); idx <= lastIdx; idx++ {
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
}
fmt.Println()
fmt.Print(" ")
for idx, p := range C[I : I+width] {
if idx+int(F[0]) < int(F[i]) {
p = 0
}
fmt.Printf("%2d ", p)
}
fmt.Println()
}
}
// Phase 3. (Optional) Backtrace to find character positions
pos := posArray(withPos, M)
j := int(F[0])
if withPos {
i := M - 1
j = maxScorePos
preferMatch := true
for {
I := i * width
j0 := j - int(F[0])
s := H[I+j0]
var s1, s2 int16
if i > 0 && j >= int(F[i]) {
s1 = H[I-width+j0-1]
}
if j > int(F[i]) {
s2 = H[I+j0-1]
}
if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j)
if i == 0 {
break
}
i--
}
preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0
j--
}
}
// Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos
}
// Implement the same sorting criteria as V2
func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern))
prevClass := charNonWord
if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1))
}
for idx := sidx; idx < eidx; idx++ {
char := text.Get(idx)
class := charClassOf(char)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
// pattern is already normalized
if normalize {
char = normalizeRune(char)
}
if char == pattern[pidx] {
if withPos {
*pos = append(*pos, idx)
}
score += scoreMatch
bonus := bonusFor(prevClass, class)
if consecutive == 0 {
firstBonus = bonus
} else {
// Break consecutive chunk
if bonus == bonusBoundary {
firstBonus = bonus
}
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
}
if pidx == 0 {
score += int(bonus * bonusFirstCharMultiplier)
} else {
score += int(bonus)
}
inGap = false
consecutive++
pidx++
} else {
if inGap {
score += scoreGapExtention
} else {
score += scoreGapStart
}
inGap = true
consecutive = 0
firstBonus = 0
}
prevClass = class
}
return score, pos
}
// FuzzyMatchV1 performs fuzzy-match
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 {
return Result{0, 0, 0}, nil
} }
// 0. (FIXME) How to find the shortest match?
// a_____b__c__abc
// ^^^^^^^^^^ ^^^
// 1. forward scan (abc)
// *-----*-----*>
// a_____b___abc__
// 2. reverse scan (cba)
// a_____b___abc__
// <***
pidx := 0 pidx := 0
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 +537,10 @@ 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) if normalize {
char = normalizeRune(char)
}
pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
@@ -157,7 +555,8 @@ 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) tidx := indexAt(index, lenRunes, forward)
char := text.Get(tidx)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -166,7 +565,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
if char == pchar { if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
@@ -175,16 +575,14 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
// Calculate the bonus. This can't be done at the same time as the
// pattern scan above because 'forward' may be false.
if !forward { if !forward {
sidx, eidx = lenRunes-eidx, lenRunes-sidx sidx, eidx = lenRunes-eidx, lenRunes-sidx
} }
return Result{int32(sidx), int32(eidx), score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} return Result{sidx, eidx, score}, pos
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
// ExactMatchNaive is a basic string searching algorithm that handles case // ExactMatchNaive is a basic string searching algorithm that handles case
@@ -192,23 +590,28 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
// of strings.ToLower + strings.Index for typical fzf use cases where input // of strings.ToLower + strings.Index for typical fzf use cases where input
// strings and patterns are not very long. // strings and patterns are not very long.
// //
// We might try to implement better algorithms in the future: // Since 0.15.0, this function searches for the match with the highest
// http://en.wikipedia.org/wiki/String_searching_algorithm // bonus point, instead of stopping immediately after finding the first match.
func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { // The solution is much cheaper since there is only one possible alignment of
// the pattern.
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}, nil
} }
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
if lenRunes < lenPattern { if lenRunes < lenPattern {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
// For simplicity, only look at the bonus at the first character position
pidx := 0 pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
for index := 0; index < lenRunes; index++ { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) index_ := indexAt(index, lenRunes, forward)
char := text.Get(index_)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -216,86 +619,133 @@ 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) if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
if pchar == char { if pchar == char {
if pidx_ == 0 {
bonus = bonusAt(text, index_)
}
pidx++ pidx++
if pidx == lenPattern { if pidx == lenPattern {
var sidx, eidx int if bonus > bestBonus {
if forward { bestPos, bestBonus = index, bonus
sidx = index - lenPattern + 1
eidx = index + 1
} else {
sidx = lenRunes - (index + 1)
eidx = lenRunes - (index - lenPattern + 1)
} }
return Result{int32(sidx), int32(eidx), if bonus == bonusBoundary {
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} break
}
index -= pidx - 1
pidx, bonus = 0, 0
} }
} else { } else {
index -= pidx index -= pidx
pidx = 0 pidx, bonus = 0, 0
} }
} }
return Result{-1, -1, 0} if bestPos >= 0 {
var sidx, eidx int
if forward {
sidx = bestPos - lenPattern + 1
eidx = bestPos + 1
} else {
sidx = lenRunes - (bestPos + 1)
eidx = lenRunes - (bestPos - lenPattern + 1)
}
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
return Result{sidx, eidx, score}, nil
}
return Result{-1, -1, 0}, nil
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(runes) < len(pattern) { if len(pattern) == 0 {
return Result{-1, -1, 0} return Result{0, 0, 0}, nil
}
if text.Length() < len(pattern) {
return Result{-1, -1, 0}, nil
} }
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)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
return Result{0, int32(lenPattern), score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} return Result{0, lenPattern, score}, nil
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
runes := util.TrimRight(input) lenRunes := text.Length()
trimmedLen := len(runes) trimmedLen := lenRunes - text.TrailingWhitespaces()
if len(pattern) == 0 {
return Result{trimmedLen, trimmedLen, 0}, nil
}
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
if diff < 0 { if diff < 0 {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
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)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen eidx := trimmedLen
return Result{int32(sidx), int32(eidx), score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} return Result{sidx, eidx, score}, nil
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Note: EqualMatch always return a zero bonus. lenPattern := len(pattern)
if len(runes) != len(pattern) { if text.Length() != lenPattern {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
runesStr := string(runes) match := true
if !caseSensitive { if normalize {
runesStr = strings.ToLower(runesStr) runes := text.ToRunes()
for idx, pchar := range pattern {
char := runes[idx]
if !caseSensitive {
char = unicode.To(unicode.LowerCase, char)
}
if normalizeRune(pchar) != normalizeRune(char) {
match = false
break
}
}
} else {
runesStr := text.ToString()
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
match = runesStr == string(pattern)
} }
if runesStr == string(pattern) { if match {
return Result{0, int32(len(pattern)), 0} return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }

View File

@@ -1,95 +1,185 @@
package algo package algo
import ( import (
"math"
"sort"
"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 Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
}
func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
if res.Start != sidx { var start, end int
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) if pos == nil || len(*pos) == 0 {
start = res.Start
end = res.End
} else {
sort.Ints(*pos)
start = (*pos)[0]
end = (*pos)[len(*pos)-1] + 1
} }
if res.End != eidx { if start != sidx {
t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", res.End, eidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern)
} }
if res.Bonus != bonus { if end != eidx {
t.Errorf("Invalid bonus: %d (expected: %d, %s / %s)", res.Bonus, bonus, input, pattern) t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern)
}
if res.Score != score {
t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern)
} }
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9, 2) for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
assertMatch(t, FuzzyMatch, false, true, "foo bar baz", "fbb", 0, 9, 8) for _, forward := range []bool{true, false} {
assertMatch(t, FuzzyMatch, false, true, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
assertMatch(t, FuzzyMatch, false, true, "/man1/zshcompctl.1", "zshc", 6, 10, 7) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
assertMatch(t, FuzzyMatch, false, true, "/.oh-my-zsh/cache", "zshc", 8, 13, 8) assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
assertMatch(t, FuzzyMatch, false, true, "ab0123 456", "12356", 3, 10, 3) scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
assertMatch(t, FuzzyMatch, false, true, "abc123 456", "12356", 3, 10, 5) bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
scoreGapStart*2+scoreGapExtention*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary)
assertMatch(t, FuzzyMatch, false, true, "foo/bar/baz", "fbb", 0, 9, 8) assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
assertMatch(t, FuzzyMatch, false, true, "fooBarBaz", "fbb", 0, 7, 6) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
assertMatch(t, FuzzyMatch, false, true, "foo barbaz", "fbb", 0, 8, 6) assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
assertMatch(t, FuzzyMatch, false, true, "fooBar Baz", "foob", 0, 4, 8) scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1, 0) scoreGapStart*2+scoreGapExtention*4)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9, 2) assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
assertMatch(t, FuzzyMatch, true, true, "Foo Bar Baz", "fbb", -1, -1, 0) scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
assertMatch(t, FuzzyMatch, true, true, "Foo/Bar/Baz", "FBB", 0, 9, 8) scoreGapStart*2+scoreGapExtention*2)
assertMatch(t, FuzzyMatch, true, true, "FooBarBaz", "FBB", 0, 7, 6) assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
assertMatch(t, FuzzyMatch, true, true, "foo BarBaz", "fBB", 0, 8, 7) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
assertMatch(t, FuzzyMatch, true, true, "FooBar Baz", "FooB", 0, 4, 8) util.Max(bonusCamel123, bonusBoundary))
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1, 0)
// Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
scoreMatch*4+bonusBoundary*3)
// Non-match
assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0)
assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0)
assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0)
}
}
} }
func TestFuzzyMatchBackward(t *testing.T) { func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4, 4) assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9, 5) scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtention)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 3)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0)
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5,
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, 7) scoreMatch*3+bonusCamel123+bonusConsecutive)
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, 10) assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
} }
} }
func TestExactMatchNaiveBackward(t *testing.T) { func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 1) assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3,
assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 1) scoreMatch*2+bonusConsecutive)
assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10,
scoreMatch*2+bonusConsecutive)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 6) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, 7) assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, 8) assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
} }
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 2)
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, 5)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9,
scoreMatch*3+bonusConsecutive*2)
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
} }
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0)
} }
} }
func TestNormalize(t *testing.T) {
caseSensitive := false
normalize := true
forward := true
test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) {
for _, fun := range funs {
assertMatch2(t, fun, caseSensitive, normalize, forward,
input, pattern, sidx, eidx, score)
}
}
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
}
func TestLongString(t *testing.T) {
bytes := make([]byte, math.MaxUint16*2)
for i := range bytes {
bytes[i] = 'x'
}
bytes[math.MaxUint16] = 'z'
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
}

424
src/algo/normalize.go Normal file
View File

@@ -0,0 +1,424 @@
// Normalization of latin script letters
// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt
package algo
var normalized map[rune]rune = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER
0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER
0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER
0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER
0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER
0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER
0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER
0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER
0x0363: 'a', // , COMBINING LATIN SMALL LETTER
0x0250: 'a', // , LATIN SMALL LETTER TURNED
0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER
0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER
0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER
0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER
0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER
0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER
0x010D: 'c', // WITH CARON, LATIN SMALL LETTER
0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER
0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0255: 'c', // WITH CURL, LATIN SMALL LETTER
0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER
0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER
0x0368: 'c', // , COMBINING LATIN SMALL LETTER
0x0297: 'c', // , LATIN LETTER STRETCHED
0x2184: 'c', // , LATIN SMALL LETTER REVERSED
0x010F: 'd', // WITH CARON, LATIN SMALL LETTER
0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER
0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0221: 'd', // WITH CURL, LATIN SMALL LETTER
0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER
0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER
0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER
0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER
0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER
0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER
0x0369: 'd', // , COMBINING LATIN SMALL LETTER
0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER
0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER
0x011B: 'e', // WITH CARON, LATIN SMALL LETTER
0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER
0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER
0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER
0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER
0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER
0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER
0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER
0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER
0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER
0x0364: 'e', // , COMBINING LATIN SMALL LETTER
0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN
0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN
0x025B: 'e', // , LATIN SMALL LETTER OPEN
0x0258: 'e', // , LATIN SMALL LETTER REVERSED
0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN
0x01DD: 'e', // , LATIN SMALL LETTER TURNED
0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN
0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER
0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER
0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER
0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER
0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER
0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER
0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER
0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER
0x0261: 'g', // , LATIN SMALL LETTER SCRIPT
0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER
0x021F: 'h', // WITH CARON, LATIN SMALL LETTER
0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER
0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER
0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED
0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER
0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER
0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER
0x036A: 'h', // , COMBINING LATIN SMALL LETTER
0x0265: 'h', // , LATIN SMALL LETTER TURNED
0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER
0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER
0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER
0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER
0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER
0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER
0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER
0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER
0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER
0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER
0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER
0x0365: 'i', // , COMBINING LATIN SMALL LETTER
0x0131: 'i', // , LATIN SMALL LETTER DOTLESS
0x1D09: 'i', // , LATIN SMALL LETTER TURNED
0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER
0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER
0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER
0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER
0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER
0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS
0x0237: 'j', // , LATIN SMALL LETTER DOTLESS
0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER
0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER
0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER
0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER
0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER
0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER
0x029E: 'k', // , LATIN SMALL LETTER TURNED
0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER
0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER
0x019A: 'l', // WITH BAR, LATIN SMALL LETTER
0x026C: 'l', // WITH BELT, LATIN SMALL LETTER
0x013E: 'l', // WITH CARON, LATIN SMALL LETTER
0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER
0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0234: 'l', // WITH CURL, LATIN SMALL LETTER
0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER
0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER
0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER
0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER
0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER
0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER
0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER
0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER
0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x036B: 'm', // , COMBINING LATIN SMALL LETTER
0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED
0x026F: 'm', // , LATIN SMALL LETTER TURNED
0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER
0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER
0x0148: 'n', // WITH CARON, LATIN SMALL LETTER
0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER
0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0235: 'n', // WITH CURL, LATIN SMALL LETTER
0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER
0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER
0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER
0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER
0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER
0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER
0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER
0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER
0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER
0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER
0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER
0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER
0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER
0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER
0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER
0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER
0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER
0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER
0x0366: 'o', // , COMBINING LATIN SMALL LETTER
0x0275: 'o', // , LATIN SMALL LETTER BARRED
0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF
0x0254: 'o', // , LATIN SMALL LETTER OPEN
0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS
0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN
0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF
0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER
0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER
0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER
0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER
0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER
0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER
0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER
0x0159: 'r', // WITH CARON, LATIN SMALL LETTER
0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER
0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER
0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER
0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED
0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER
0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER
0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER
0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER
0x036C: 'r', // , COMBINING LATIN SMALL LETTER
0x0279: 'r', // , LATIN SMALL LETTER TURNED
0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER
0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER
0x0161: 's', // WITH CARON, LATIN SMALL LETTER
0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER
0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER
0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG
0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER
0x0282: 's', // WITH HOOK, LATIN SMALL LETTER
0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER
0x017F: 's', // , LATIN SMALL LETTER LONG
0x00DF: 's', // , LATIN SMALL LETTER SHARP
0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER
0x0165: 't', // WITH CARON, LATIN SMALL LETTER
0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER
0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER
0x0236: 't', // WITH CURL, LATIN SMALL LETTER
0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER
0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER
0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER
0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER
0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0167: 't', // WITH STROKE, LATIN SMALL LETTER
0x036D: 't', // , COMBINING LATIN SMALL LETTER
0x0287: 't', // , LATIN SMALL LETTER TURNED
0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER
0x0289: 'u', // BAR, LATIN SMALL LETTER
0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER
0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER
0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER
0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER
0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER
0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER
0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER
0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER
0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER
0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER
0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER
0x0367: 'u', // , COMBINING LATIN SMALL LETTER
0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS
0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER
0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER
0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER
0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER
0x036E: 'v', // , COMBINING LATIN SMALL LETTER
0x028C: 'v', // , LATIN SMALL LETTER TURNED
0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER
0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER
0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER
0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER
0x028D: 'w', // , LATIN SMALL LETTER TURNED
0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER
0x036F: 'x', // , COMBINING LATIN SMALL LETTER
0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER
0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER
0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER
0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER
0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER
0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER
0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER
0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER
0x028E: 'y', // , LATIN SMALL LETTER TURNED
0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER
0x017E: 'z', // WITH CARON, LATIN SMALL LETTER
0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0291: 'z', // WITH CURL, LATIN SMALL LETTER
0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER
0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER
0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER
0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER
0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER
0x0251: 'a', // , latin small letter script
0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER
0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER
0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER
0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER
0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER
0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL
0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER
0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER
0x0299: 'B', // , LATIN LETTER SMALL CAPITAL
0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED
0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER
0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER
0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL
0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER
0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN
0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL
0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER
0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER
0x0190: 'E', // , LATIN CAPITAL LETTER OPEN
0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED
0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL
0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER
0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL
0x0262: 'G', // , LATIN LETTER SMALL CAPITAL
0x029C: 'H', // , LATIN LETTER SMALL CAPITAL
0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER
0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER
0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER
0x026A: 'I', // , LATIN LETTER SMALL CAPITAL
0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER
0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL
0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL
0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER
0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL
0x029F: 'L', // , LATIN LETTER SMALL CAPITAL
0x019C: 'M', // , LATIN CAPITAL LETTER TURNED
0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL
0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER
0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER
0x0274: 'N', // , LATIN LETTER SMALL CAPITAL
0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED
0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER
0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER
0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER
0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER
0x0186: 'O', // , LATIN CAPITAL LETTER OPEN
0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL
0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN
0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL
0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER
0x0280: 'R', // , LATIN LETTER SMALL CAPITAL
0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED
0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED
0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED
0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL
0x0244: 'U', // BAR, LATIN CAPITAL LETTER
0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER
0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER
0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL
0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER
0x0245: 'V', // , LATIN CAPITAL LETTER TURNED
0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL
0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL
0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER
0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
}
// NormalizeRunes normalizes latin script letters
func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes))
copy(ret, runes)
for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 {
continue
}
n := normalized[r]
if n > 0 {
ret[idx] = normalized[r]
}
}
return ret
}

View File

@@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/tui"
) )
type ansiOffset struct { type ansiOffset struct {
@@ -14,29 +16,38 @@ type ansiOffset struct {
} }
type ansiState struct { type ansiState struct {
fg int fg tui.Color
bg int bg tui.Color
bold bool attr tui.Attr
} }
func (s *ansiState) colored() bool { func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold return s.fg != -1 || s.bg != -1 || s.attr > 0
} }
func (s *ansiState) equals(t *ansiState) bool { func (s *ansiState) equals(t *ansiState) bool {
if t == nil { if t == nil {
return !s.colored() return !s.colored()
} }
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
} }
var ansiRegex *regexp.Regexp var ansiRegex *regexp.Regexp
func init() { func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") /*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x08\x0e\x0f]")
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
@@ -49,7 +60,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
prev := str[idx:offset[0]] prev := str[idx:offset[0]]
output.WriteString(prev) output.WriteString(prev)
if proc != nil && !proc(prev, state) { if proc != nil && !proc(prev, state) {
break return "", nil, nil
} }
newState := interpretCode(str[offset[0]:offset[1]], state) newState := interpretCode(str[offset[0]:offset[1]], state)
@@ -84,18 +95,21 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if proc != nil { if proc != nil {
proc(rest, state) proc(rest, state)
} }
return output.String(), offsets, 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 {
// State // State
var state *ansiState var state *ansiState
if prevState == nil { if prevState == nil {
state = &ansiState{-1, -1, false} state = &ansiState{-1, -1, 0}
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold} state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
} }
if ansiCode[len(ansiCode)-1] == 'K' { if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
return state return state
} }
@@ -105,7 +119,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
init := func() { init := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.bold = false state.attr = 0
state256 = 0 state256 = 0
} }
@@ -129,28 +143,56 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49: case 49:
state.bg = -1 state.bg = -1
case 1: case 1:
state.bold = true state.attr = state.attr | tui.Bold
case 2:
state.attr = state.attr | tui.Dim
case 3:
state.attr = state.attr | tui.Italic
case 4:
state.attr = state.attr | tui.Underline
case 5:
state.attr = state.attr | tui.Blink
case 7:
state.attr = state.attr | tui.Reverse
case 0: case 0:
init() init()
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
state.fg = num - 30 state.fg = tui.Color(num - 30)
} else if num >= 40 && num <= 47 { } else if num >= 40 && num <= 47 {
state.bg = num - 40 state.bg = tui.Color(num - 40)
} else if num >= 90 && num <= 97 {
state.fg = tui.Color(num - 90 + 8)
} else if num >= 100 && num <= 107 {
state.bg = tui.Color(num - 100 + 8)
} }
} }
case 1: case 1:
switch num { switch num {
case 2:
state256 = 10 // MAGIC
case 5: case 5:
state256++ state256++
default: default:
state256 = 0 state256 = 0
} }
case 2: case 2:
*ptr = num *ptr = tui.Color(num)
state256 = 0
case 10:
*ptr = tui.Color(1<<24) | tui.Color(num<<16)
state256++
case 11:
*ptr = *ptr | tui.Color(num<<8)
state256++
case 12:
*ptr = *ptr | tui.Color(num)
state256 = 0 state256 = 0
} }
} }
} }
if state256 > 0 {
*ptr = -1
}
return state return state
} }

View File

@@ -3,150 +3,156 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/tui"
) )
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr tui.Attr
if bold {
attr = tui.Bold
}
if offset.offset[0] != b || offset.offset[1] != e || if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
t.Error(offset, b, e, fg, bg, bold) t.Error(offset, b, e, fg, bg, attr)
} }
} }
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, nil) 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: %s %s", output, []rune(output))
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
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[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
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.attr == 0 {
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.attr == 0 {
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.attr > 0 {
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,14 +3,16 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
// FIXME global // FIXME global
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byScore, 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,26 +8,33 @@ import (
const ( const (
// Current version // Current version
version = "0.13.1" version = "0.16.1"
// 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/^..//` readerBufferSize = 64 * 1024
// Terminal // Terminal
initialDelay = 20 * time.Millisecond initialDelay = 20 * time.Millisecond
initialDelayTac = 100 * time.Millisecond initialDelayTac = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond spinnerDuration = 200 * time.Millisecond
maxPatternLength = 100
// Matcher // Matcher
progressMinDuration = 200 * time.Millisecond numPartitionsMultiplier = 8
maxPartitions = 32
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk // Capacity of each chunk
chunkSize int = 100 chunkSize int = 100
// Pre-allocated memory slices to minimize GC
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
slab32Size int = 2048 // 8KB * 32 = 256KB
// Do not cache results of low selectivity queries // Do not cache results of low selectivity queries
queryCacheMax int = chunkSize / 5 queryCacheMax int = chunkSize / 5

8
src/constants_unix.go Normal file
View File

@@ -0,0 +1,8 @@
// +build !windows
package fzf
const (
// Reader
defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
)

8
src/constants_windows.go Normal file
View File

@@ -0,0 +1,8 @@
// +build windows
package fzf
const (
// Reader
defaultCommand = `dir /s/b`
)

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, nil) 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, 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,27 +143,30 @@ 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.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(*opts.Filter) opts.Printer(*opts.Filter)
} }
pattern := patternBuilder([]rune(*opts.Filter)) pattern := patternBuilder([]rune(*opts.Filter))
found := false found := false
if streamingFilter { if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size)
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, false, slab); result != nil {
found = true opts.Printer(item.text.ToString())
found = true
}
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
@@ -185,7 +180,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)) opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
found = true found = true
} }
} }
@@ -259,13 +254,13 @@ func Run(opts *Options) {
} else if val.final { } else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(opts.Query) opts.Printer(opts.Query)
} }
if len(opts.Expect) > 0 { if len(opts.Expect) > 0 {
fmt.Println() opts.Printer("")
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) os.Exit(exitOk)

View File

@@ -1,704 +0,0 @@
package curses
/*
#include <ncurses.h>
#include <locale.h>
#cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
SCREEN *c_newterm () {
return newterm(NULL, stderr, stdin);
}
*/
import "C"
import (
"fmt"
"os"
"strings"
"syscall"
"time"
"unicode/utf8"
)
// Types of user action
const (
Rune = iota
CtrlA
CtrlB
CtrlC
CtrlD
CtrlE
CtrlF
CtrlG
CtrlH
Tab
CtrlJ
CtrlK
CtrlL
CtrlM
CtrlN
CtrlO
CtrlP
CtrlQ
CtrlR
CtrlS
CtrlT
CtrlU
CtrlV
CtrlW
CtrlX
CtrlY
CtrlZ
ESC
Invalid
Mouse
DoubleClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
// Pallete
const (
ColNormal = iota
ColPrompt
ColMatch
ColCurrent
ColCurrentMatch
ColSpinner
ColInfo
ColCursor
ColSelected
ColHeader
ColBorder
ColUser // Should be the last entry
)
const (
doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
)
type ColorTheme struct {
UseDefault bool
Fg int16
Bg int16
DarkBg int16
Prompt int16
Match int16
Current int16
CurrentMatch int16
Spinner int16
Info int16
Cursor int16
Selected int16
Header int16
Border int16
}
type Event struct {
Type int
Char rune
MouseEvent *MouseEvent
}
type MouseEvent struct {
Y int
X int
S int
Down bool
Double bool
Mod bool
}
var (
_buf []byte
_in *os.File
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
_screen *C.SCREEN
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
FG int
CurrentFG int
BG 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() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: C.COLOR_BLACK,
Prompt: C.COLOR_BLUE,
Match: C.COLOR_GREEN,
Current: C.COLOR_YELLOW,
CurrentMatch: C.COLOR_GREEN,
Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109,
Border: 59}
Light256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
}
func attrColored(pair int, bold bool) C.int {
var attr C.int
if pair > ColNormal {
attr = C.COLOR_PAIR(C.int(pair))
}
if bold {
attr = attr | C.A_BOLD
}
return attr
}
func attrMono(pair int, bold bool) C.int {
var attr C.int
switch pair {
case ColCurrent:
if bold {
attr = C.A_REVERSE
}
case ColMatch:
attr = C.A_UNDERLINE
case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE
}
if bold {
attr = attr | C.A_BOLD
}
return attr
}
func MaxX() int {
return int(C.COLS)
}
func MaxY() int {
return int(C.LINES)
}
func getch(nonblock bool) int {
b := make([]byte, 1)
syscall.SetNonblock(int(_in.Fd()), nonblock)
_, err := _in.Read(b)
if err != nil {
return -1
}
return int(b[0])
}
func Init(theme *ColorTheme, black bool, mouse bool) {
{
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open /dev/tty")
}
_in = in
// Break STDIN
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
}
C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.c_newterm()
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2)
}
C.set_term(_screen)
if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
}
C.noecho()
C.raw() // stty dsusp undef
if theme != nil {
C.start_color()
var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
baseTheme = Dark256
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored
} else {
_color = attrMono
}
}
func override(a int16, b int16) C.short {
if b == colUndefined {
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 {
bg = C.COLOR_BLACK
} else if theme.UseDefault {
fg = colDefault
bg = colDefault
C.use_default_colors()
}
if theme.UseDefault {
FG = colDefault
BG = colDefault
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
}
currentFG := override(baseTheme.Current, theme.Current)
darkBG := override(baseTheme.DarkBg, theme.DarkBg)
CurrentFG = int(currentFG)
DarkBG = int(darkBG)
C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
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() {
C.endwin()
C.delscreen(_screen)
}
func GetBytes() []byte {
c := getch(false)
_buf = append(_buf, byte(c))
for {
c = getch(true)
if c == -1 {
break
}
_buf = append(_buf, byte(c))
}
return _buf
}
// 27 (91 79) 77 type x y
func mouseSequence(sz *int) Event {
if len(_buf) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := _buf[3] >= 36
down := _buf[3]%2 == 0
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
}
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2
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}
}
func escSequence(sz *int) Event {
if len(_buf) < 2 {
return Event{ESC, 0, nil}
}
*sz = 2
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:
return Event{AltB, 0, nil}
case 100:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
return Event{AltBS, 0, nil}
case 91, 79:
if len(_buf) < 3 {
return Event{Invalid, 0, nil}
}
*sz = 3
switch _buf[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{Invalid, 0, nil}
}
*sz = 4
switch _buf[2] {
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
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch _buf[3] {
case 126:
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:
if len(_buf) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[4] {
case 50:
switch _buf[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil}
}
func GetChar() Event {
if len(_buf) == 0 {
_buf = GetBytes()
}
if len(_buf) == 0 {
panic("Empty _buffer")
}
sz := 1
defer func() {
_buf = _buf[sz:]
}()
switch _buf[0] {
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
case ESC:
return escSequence(&sz)
}
// CTRL-A ~ CTRL-Z
if _buf[0] <= CtrlZ {
return Event{int(_buf[0]), 0, nil}
}
r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil}
}
func (w *Window) Close() {
C.delwin(w.win)
}
func (w *Window) Enclose(y int, x int) bool {
return bool(C.wenclose(w.win, C.int(y), C.int(x)))
}
func (w *Window) Move(y int, x int) {
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 {
return -1
}
return r
}, text)))
}
func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold)
C.wattron(w.win, attr)
w.Print(text)
C.wattroff(w.win, attr)
}
func Clear() {
C.clear()
}
func Endwin() {
C.endwin()
}
func 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 {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

View File

@@ -1,14 +0,0 @@
package curses
import (
"testing"
)
func TestPairFor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) {
t.Fail()
}
if PairFor(-1, 10) != PairFor(-1, 10) {
t.Fail()
}
}

View File

@@ -1,7 +1,10 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os"
"os/user" "os/user"
"runtime"
"testing" "testing"
) )
@@ -10,23 +13,34 @@ func TestHistory(t *testing.T) {
// Invalid arguments // Invalid arguments
user, _ := user.Current() user, _ := user.Current()
paths := []string{"/etc", "/proc"} var paths []string
if user.Name != "root" { if runtime.GOOS == "windows" {
paths = append(paths, "/etc/sudoers") // GOPATH should exist, so we shouldn't be able to override it
paths = []string{os.Getenv("GOPATH")}
} else {
paths = []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
} }
for _, path := range paths { for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil { if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path) t.Error("Error expected for: " + path)
} }
} }
f, _ := ioutil.TempFile("", "fzf-history")
f.Close()
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
for i := 0; i < maxHistory+10; i++ { for i := 0; i < maxHistory+10; i++ {
h.append("foobar") h.append("foobar")
} }
} }
{ // Read lines { // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }
@@ -37,13 +51,13 @@ func TestHistory(t *testing.T) {
} }
} }
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
h.append("barfoo") h.append("barfoo")
h.append("") h.append("")
h.append("foobarbaz") h.append("foobarbaz")
} }
{ // Read lines again { // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }

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, 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

@@ -26,6 +26,7 @@ type Matcher struct {
eventBox *util.EventBox eventBox *util.EventBox
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
} }
@@ -37,13 +38,15 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: partitions,
slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger)}
} }
@@ -106,18 +109,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 +131,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) {
@@ -152,17 +156,26 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
for idx, chunks := range slices { for idx, chunks := range slices {
waitGroup.Add(1) waitGroup.Add(1)
go func(idx int, chunks []*Chunk) { if m.slab[idx] == nil {
m.slab[idx] = util.MakeSlab(slab16Size, slab32Size)
}
go func(idx int, slab *util.Slab, chunks []*Chunk) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
sliceMatches := []*Item{} count := 0
for _, chunk := range chunks { allMatches := make([][]*Result, len(chunks))
matches := request.pattern.Match(chunk) for idx, chunk := range chunks {
sliceMatches = append(sliceMatches, matches...) matches := request.pattern.Match(chunk, slab)
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))
@@ -171,7 +184,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks) }(idx, m.slab[idx], chunks)
} }
wait := func() bool { wait := func() bool {
@@ -199,12 +212,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

@@ -8,7 +8,9 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@@ -19,8 +21,10 @@ const usage = `usage: fzf [options]
-x, --extended Extended-search mode -x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match -e, --exact Enable Exact-match
--algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
--literal Do not normalize latin script letters before matching
-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]).
@@ -41,9 +45,14 @@ const usage = `usage: fzf [options]
--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)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump and jump-accept
Layout Layout
--height=HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen
--min-height=HEIGHT Minimum height when --height is given in percent
(default: 10)
--reverse Reverse orientation --reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
@@ -55,6 +64,7 @@ const usage = `usage: fzf [options]
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8) --tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--no-bold Do not use bold text
History History
--history=FILE History file --history=FILE History file
@@ -63,7 +73,7 @@ const usage = `usage: fzf [options]
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden] [up|down|left|right][:SIZE[%]][:wrap][:hidden]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -94,7 +104,7 @@ const (
type criterion int type criterion int
const ( const (
byMatchLen criterion = iota byScore criterion = iota
byLength byLength
byBegin byBegin
byEnd byEnd
@@ -123,13 +133,16 @@ type previewOpts struct {
position windowPosition position windowPosition
size sizeSpec size sizeSpec
hidden bool hidden bool
wrap bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo
Extended bool Extended bool
Case Case Case Case
Normalize bool
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter Delimiter Delimiter Delimiter
@@ -139,12 +152,16 @@ type Options struct {
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *tui.ColorTheme
Black bool Black bool
Bold bool
Height sizeSpec
MinHeight int
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
FileWord bool
InlineInfo bool InlineInfo bool
JumpLabels string JumpLabels string
Prompt string Prompt string
@@ -159,6 +176,7 @@ type Options struct {
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Printer func(string)
Sync bool Sync bool
History *History History *History
Header []string Header []string
@@ -171,23 +189,28 @@ type Options struct {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true, Extended: true,
Case: CaseSmart, Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Criteria: []criterion{byMatchLen, byLength}, Criteria: []criterion{byScore, byLength},
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: curses.EmptyTheme(), Theme: tui.EmptyTheme(),
Black: false, Black: false,
Bold: true,
MinHeight: 10,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
FileWord: false,
InlineInfo: false, InlineInfo: false,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
@@ -199,9 +222,10 @@ 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}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) },
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
@@ -240,7 +264,7 @@ func nextString(args []string, i *int, message string) string {
} }
func optionalNextString(args []string, i *int) string { func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 { if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") {
*i++ *i++
return args[*i] return args[*i]
} }
@@ -321,6 +345,22 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
func parseAlgo(str string) algo.Algo {
switch str {
case "v1":
return algo.FuzzyMatchV1
case "v2":
return algo.FuzzyMatchV2
default:
errorExit("invalid algorithm (expected: v1 or v2)")
}
return algo.FuzzyMatchV2
}
func parseKeyChords(str string, message string) map[int]string { func parseKeyChords(str string, message string) map[int]string {
if len(str) == 0 { if len(str) == 0 {
errorExit(message) errorExit(message)
@@ -340,60 +380,66 @@ func parseKeyChords(str string, message string) map[int]string {
chord := 0 chord := 0
switch lkey { switch lkey {
case "up": case "up":
chord = curses.Up chord = tui.Up
case "down": case "down":
chord = curses.Down chord = tui.Down
case "left": case "left":
chord = curses.Left chord = tui.Left
case "right": case "right":
chord = curses.Right chord = tui.Right
case "enter", "return": case "enter", "return":
chord = curses.CtrlM chord = tui.CtrlM
case "space": case "space":
chord = curses.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = curses.BSpace chord = tui.BSpace
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = curses.AltEnter chord = tui.AltEnter
case "alt-space": case "alt-space":
chord = curses.AltSpace chord = tui.AltSpace
case "alt-/": case "alt-/":
chord = curses.AltSlash chord = tui.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = curses.AltBS chord = tui.AltBS
case "tab": case "tab":
chord = curses.Tab chord = tui.Tab
case "btab", "shift-tab": case "btab", "shift-tab":
chord = curses.BTab chord = tui.BTab
case "esc": case "esc":
chord = curses.ESC chord = tui.ESC
case "del": case "del":
chord = curses.Del chord = tui.Del
case "home": case "home":
chord = curses.Home chord = tui.Home
case "end": case "end":
chord = curses.End chord = tui.End
case "pgup", "page-up": case "pgup", "page-up":
chord = curses.PgUp chord = tui.PgUp
case "pgdn", "page-down": case "pgdn", "page-down":
chord = curses.PgDn chord = tui.PgDn
case "shift-left": case "shift-left":
chord = curses.SLeft chord = tui.SLeft
case "shift-right": case "shift-right":
chord = curses.SRight chord = tui.SRight
case "double-click": case "double-click":
chord = curses.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
chord = curses.F10 chord = tui.F10
case "f11":
chord = tui.F11
case "f12":
chord = tui.F12
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 = tui.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 = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) {
chord = tui.Alt0 + int(lkey[4]) - '0'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1' chord = tui.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 = tui.AltZ + int([]rune(key)[0])
} else { } else {
errorExit("unsupported key: " + key) errorExit("unsupported key: " + key)
} }
@@ -406,7 +452,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{byScore}
hasIndex := false hasIndex := false
hasLength := false hasLength := false
hasBegin := false hasBegin := false
@@ -440,7 +486,7 @@ func parseTiebreak(str string) []criterion {
return criteria return criteria
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
if theme != nil { if theme != nil {
dupe := *theme dupe := *theme
return &dupe return &dupe
@@ -448,16 +494,17 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
return nil return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
for _, str := range strings.Split(strings.ToLower(str), ",") { for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str { switch str {
case "dark": case "dark":
theme = dupeTheme(curses.Dark256) theme = dupeTheme(tui.Dark256)
case "light": case "light":
theme = dupeTheme(curses.Light256) theme = dupeTheme(tui.Light256)
case "16": case "16":
theme = dupeTheme(curses.Default16) theme = dupeTheme(tui.Default16)
case "bw", "no": case "bw", "no":
theme = nil theme = nil
default: default:
@@ -473,18 +520,22 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
if len(pair) != 2 { if len(pair) != 2 {
fail() fail()
} }
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 { var ansi tui.Color
fail() if rrggbb.MatchString(pair[1]) {
ansi = tui.HexToColor(pair[1])
} else {
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi = tui.Color(ansi32)
} }
ansi := int16(ansi32)
switch pair[0] { switch pair[0] {
case "fg": case "fg":
theme.Fg = ansi theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg": case "bg":
theme.Bg = ansi theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+": case "fg+":
theme.Current = ansi theme.Current = ansi
case "bg+": case "bg+":
@@ -556,9 +607,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
} }
var key int var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon { if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ key = ',' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
@@ -637,6 +688,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actPageUp keymap[key] = actPageUp
case "page-down": case "page-down":
keymap[key] = actPageDown keymap[key] = actPageDown
case "half-page-up":
keymap[key] = actHalfPageUp
case "half-page-down":
keymap[key] = actHalfPageDown
case "previous-history": case "previous-history":
keymap[key] = actPreviousHistory keymap[key] = actPreviousHistory
case "next-history": case "next-history":
@@ -645,6 +700,14 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actTogglePreview keymap[key] = actTogglePreview
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
case "preview-up":
keymap[key] = actPreviewUp
case "preview-down":
keymap[key] = actPreviewDown
case "preview-page-up":
keymap[key] = actPreviewPageUp
case "preview-page-down":
keymap[key] = actPreviewPageDown
default: default:
if isExecuteAction(actLower) { if isExecuteAction(actLower) {
var offset int var offset int
@@ -722,39 +785,52 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
return sizeSpec{val, percent} return sizeSpec{val, percent}
} }
func parseHeight(str string) sizeSpec {
if util.IsWindows() {
errorExit("--height options is currently not supported on Windows")
}
size := parseSize(str, 100, "height")
return size
}
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
layout := input // Default
if strings.HasSuffix(layout, ":hidden") { opts.position = posRight
opts.hidden = true opts.size = sizeSpec{50, true}
layout = strings.TrimSuffix(layout, ":hidden") opts.hidden = false
} opts.wrap = false
tokens := strings.Split(layout, ":") tokens := strings.Split(input, ":")
if len(tokens) == 0 || len(tokens) > 2 { sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$")
errorExit("invalid window layout: " + input) for _, token := range tokens {
} switch token {
case "hidden":
if len(tokens) > 1 { opts.hidden = true
opts.size = parseSize(tokens[1], 99, "window size") case "wrap":
} else { opts.wrap = true
opts.size = sizeSpec{50, true} case "up", "top":
opts.position = posUp
case "down", "bottom":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
} else {
errorExit("invalid preview window layout: " + input)
}
}
} }
if !opts.size.percent && opts.size.size > 0 { if !opts.size.percent && opts.size.size > 0 {
// Adjust size for border // Adjust size for border
opts.size.size += 2 opts.size.size += 2
} // And padding
if opts.position == posLeft || opts.position == posRight {
switch tokens[0] { opts.size.size += 2
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)
} }
} }
@@ -832,6 +908,12 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter": case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--literal":
opts.Normalize = false
case "--no-literal":
opts.Normalize = true
case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
case "--expect": case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak": case "--tiebreak":
@@ -841,7 +923,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 = curses.EmptyTheme() opts.Theme = tui.EmptyTheme()
} else { } else {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
@@ -878,11 +960,15 @@ func parseOptions(opts *Options, allArgs []string) {
case "+c", "--no-color": case "+c", "--no-color":
opts.Theme = nil opts.Theme = nil
case "+2", "--no-256": case "+2", "--no-256":
opts.Theme = curses.Default16 opts.Theme = tui.Default16
case "--black": case "--black":
opts.Black = true opts.Black = true
case "--no-black": case "--no-black":
opts.Black = false opts.Black = false
case "--bold":
opts.Bold = true
case "--no-bold":
opts.Bold = false
case "--reverse": case "--reverse":
opts.Reverse = true opts.Reverse = true
case "--no-reverse": case "--no-reverse":
@@ -897,6 +983,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = false opts.Hscroll = false
case "--hscroll-off": case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
@@ -916,6 +1006,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ReadZero = true opts.ReadZero = true
case "--no-read0": case "--no-read0":
opts.ReadZero = false opts.ReadZero = false
case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") }
case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) }
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":
@@ -949,7 +1043,13 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]"))
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height":
opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
case "--no-height":
opts.Height = sizeSpec{}
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--margin": case "--margin":
@@ -960,7 +1060,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
if match, value := optString(arg, "-q", "--query="); match { if match, value := optString(arg, "--algo="); match {
opts.FuzzyAlgo = parseAlgo(value)
} else if match, value := optString(arg, "-q", "--query="); match {
opts.Query = value opts.Query = value
} else if match, value := optString(arg, "-f", "--filter="); match { } else if match, value := optString(arg, "-f", "--filter="); match {
opts.Filter = &value opts.Filter = &value
@@ -974,6 +1076,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s", "--sort="); match { } else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--height="); match {
opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--min-height="); match {
opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
@@ -1038,11 +1144,11 @@ func parseOptions(opts *Options, allArgs []string) {
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[curses.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[curses.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = actPreviousHistory
} }
if _, prs := opts.Keymap[curses.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[curses.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = actNextHistory
} }
} }

View File

@@ -2,9 +2,11 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
@@ -42,24 +44,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))
} }
} }
@@ -132,48 +134,48 @@ func TestParseKeys(t *testing.T) {
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(curses.CtrlZ, "ctrl-z") check(tui.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z") check(tui.AltZ, "alt-z")
check(curses.F2, "f2") check(tui.F2, "f2")
check(curses.AltZ+'@', "@") check(tui.AltZ+'@', "@")
check(curses.AltA, "Alt-a") check(tui.AltA, "Alt-a")
check(curses.AltZ+'!', "!") check(tui.AltZ+'!', "!")
check(curses.CtrlA+'g'-'a', "ctrl-G") check(tui.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J") check(tui.AltZ+'J', "J")
check(curses.AltZ+'g', "g") check(tui.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter") check(tui.AltEnter, "ALT-enter")
check(curses.AltSpace, "alt-SPACE") check(tui.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", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
check(curses.CtrlM, "Return") check(tui.CtrlM, "Return")
check(curses.AltZ+' ', "space") check(tui.AltZ+' ', "space")
check(curses.Tab, "tab") check(tui.Tab, "tab")
check(curses.BTab, "btab") check(tui.BTab, "btab")
check(curses.ESC, "esc") check(tui.ESC, "esc")
check(curses.Up, "up") check(tui.Up, "up")
check(curses.Down, "down") check(tui.Down, "down")
check(curses.Left, "left") check(tui.Left, "left")
check(curses.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(curses.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(curses.PgUp, "page-up") check(tui.PgUp, "page-up")
check(curses.PgDn, "Page-Down") check(tui.PgDn, "Page-Down")
check(curses.Home, "Home") check(tui.Home, "Home")
check(curses.End, "End") check(tui.End, "End")
check(curses.AltBS, "Alt-BSpace") check(tui.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left") check(tui.SLeft, "shift-left")
check(curses.SRight, "shift-right") check(tui.SRight, "shift-right")
check(curses.BTab, "shift-tab") check(tui.BTab, "shift-tab")
check(curses.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(curses.BSpace, "bspace") check(tui.BSpace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
@@ -190,36 +192,36 @@ func TestParseKeysWithComma(t *testing.T) {
pairs := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+'c', "c") check(pairs, tui.AltZ+'c', "c")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
@@ -235,41 +237,41 @@ func TestBind(t *testing.T) {
} }
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string) execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA]) check(actBeginningOfLine, keymap[tui.CtrlA])
parseKeymap(keymap, execmap, parseKeymap(keymap, execmap,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
check(actKillLine, keymap[curses.CtrlA]) check(actKillLine, keymap[tui.CtrlA])
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[tui.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[tui.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+',']) check(actAbort, keymap[tui.AltZ+','])
check(actAccept, keymap[curses.AltZ+':']) check(actAccept, keymap[tui.AltZ+':'])
check(actPageDown, keymap[curses.AltZ]) check(actPageDown, keymap[tui.AltZ])
check(actExecute, keymap[curses.F1]) check(actExecute, keymap[tui.F1])
check(actExecute, keymap[curses.F2]) check(actExecute, keymap[tui.F2])
check(actExecute, keymap[curses.F3]) check(actExecute, keymap[tui.F3])
check(actExecute, keymap[curses.F4]) check(actExecute, keymap[tui.F4])
checkString("ls {}", execmap[curses.F1]) checkString("ls {}", execmap[tui.F1])
checkString("echo {}, {}, {}", execmap[curses.F2]) checkString("echo {}, {}, {}", execmap[tui.F2])
checkString("echo '({})'", execmap[curses.F3]) checkString("echo '({})'", execmap[tui.F3])
checkString("less {}", execmap[curses.F4]) checkString("less {}", execmap[tui.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X'])
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, execmap, "f1:abort")
check(actAbort, keymap[curses.F1]) check(actAbort, keymap[tui.F1])
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := curses.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
@@ -282,7 +284,7 @@ func TestColorSpec(t *testing.T) {
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
if *light != *curses.Light256 { if *light != *tui.Light256 {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
if light == theme { if light == theme {
@@ -293,29 +295,23 @@ func TestColorSpec(t *testing.T) {
if customized.Fg != 231 || customized.Bg != 232 { if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
if *curses.Dark256 == *customized { if *tui.Dark256 == *customized {
t.Errorf("colors should not be equivalent") t.Errorf("colors should not be equivalent")
} }
customized.Fg = curses.Dark256.Fg customized.Fg = tui.Dark256.Fg
customized.Bg = curses.Dark256.Bg customized.Bg = tui.Dark256.Bg
if *curses.Dark256 == *customized { if *tui.Dark256 != *customized {
t.Errorf("colors should now be equivalent") t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
} }
func TestParseNilTheme(t *testing.T) { func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme var theme *tui.ColorTheme
newTheme := parseTheme(theme, "prompt:12") newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil { if newTheme != nil {
t.Errorf("color is disabled. keep it that way.") t.Errorf("color is disabled. keep it that way.")
@@ -335,31 +331,33 @@ func TestDefaultCtrlNP(t *testing.T) {
t.Error() t.Error()
} }
} }
check([]string{}, curses.CtrlN, actDown) check([]string{}, tui.CtrlN, actDown)
check([]string{}, curses.CtrlP, actUp) check([]string{}, tui.CtrlP, actUp)
check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
hist := "--history=/tmp/foo" f, _ := ioutil.TempFile("", "fzf-history")
check([]string{hist}, curses.CtrlN, actNextHistory) f.Close()
check([]string{hist}, curses.CtrlP, actPreviousHistory) hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
}
func optsFor(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
} }
func TestToggle(t *testing.T) { func TestToggle(t *testing.T) {
optsFor := func(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
}
opts := optsFor() opts := optsFor()
if opts.ToggleSort { if opts.ToggleSort {
t.Error() t.Error()
@@ -375,3 +373,42 @@ 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.wrap == 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:wrap")
if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.position == posLeft &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2+2) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.wrap == false &&
opts.Preview.position == posDown &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=up:15:wrap:hidden")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.position == posUp &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2) {
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"
@@ -41,15 +40,17 @@ type termSet []term
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
fuzzy bool fuzzy bool
fuzzyAlgo algo.Algo
extended bool extended bool
caseSensitive bool caseSensitive bool
normalize bool
forward bool forward bool
text []rune text []rune
termSets []termSet termSets []termSet
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]algo.Algo
} }
var ( var (
@@ -75,8 +76,8 @@ 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, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, 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,17 +91,17 @@ 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 {
termSets = parseTerms(fuzzy, caseMode, asString) termSets = parseTerms(fuzzy, caseMode, normalize, asString)
Loop: Loop:
for _, termSet := range termSets { for _, termSet := range termSets {
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
} }
@@ -117,17 +118,19 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
ptr := &Pattern{ ptr := &Pattern{
fuzzy: fuzzy, fuzzy: fuzzy,
fuzzyAlgo: fuzzyAlgo,
extended: extended, extended: extended,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
normalize: normalize,
forward: forward, forward: forward,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
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]algo.Algo)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termExact] = algo.ExactMatchNaive
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
@@ -137,7 +140,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return ptr return ptr
} }
func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
@@ -162,12 +165,13 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
if strings.HasPrefix(text, "!") { if strings.HasPrefix(text, "!") {
inv = true inv = true
typ = termExact
text = text[1:] text = text[1:]
} }
if strings.HasPrefix(text, "'") { if strings.HasPrefix(text, "'") {
// Flip exactness // Flip exactness
if fuzzy { if fuzzy && !inv {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else { } else {
@@ -192,10 +196,14 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
sets = append(sets, set) sets = append(sets, set)
set = termSet{} set = termSet{}
} }
textRunes := []rune(text)
if normalize {
textRunes = algo.NormalizeRunes(textRunes)
}
set = append(set, term{ set = append(set, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: textRunes,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
origText: origText}) origText: origText})
switchSet = true switchSet = true
@@ -235,9 +243,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, slab *util.Slab) []*Result {
space := chunk
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
@@ -246,7 +252,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 +263,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, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@@ -271,20 +277,19 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*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, false, slab); 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, false, slab); match != nil {
matches = append(matches, dupItem(item, offsets, bonus)) matches = append(matches, match)
} }
} }
} }
@@ -292,63 +297,75 @@ 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, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if !p.extended { if p.extended {
offset, _ := p.basicMatch(item) if offsets, bonus, trimLen, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
sidx := offset[0] return buildResult(item, offsets, bonus, trimLen), offsets, pos
return sidx >= 0 }
return nil, nil, nil
} }
offsets, _ := p.extendedMatch(item) offset, bonus, trimLen, pos := p.basicMatch(item, withPos, slab)
return len(offsets) == len(p.termSets) if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
return buildResult(item, offsets, bonus, trimLen), offsets, pos
}
return nil, nil, nil
} }
func dupItem(item *Item, offsets []Offset, bonus int32) *Item { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, 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(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
var totalBonus int32 var totalScore int
var totalTrimLen int
var allPos *[]int
if withPos {
allPos = &[]int{}
}
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
var offset *Offset var offset Offset
var bonus int32 var currentScore 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, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
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, currentScore, trimLen = off, score, tLen
matched = true
if withPos {
if pos != nil {
*allPos = append(*allPos, *pos...)
} else {
for idx := off[0]; idx < off[1]; idx++ {
*allPos = append(*allPos, int(idx))
}
}
}
break break
} else if term.inv { } else if term.inv {
offset, bonus = &Offset{0, 0, 0}, 0 offset, currentScore, 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 totalScore += currentScore
totalTrimLen += trimLen
} }
} }
return offsets, totalBonus return offsets, totalScore, totalTrimLen, allPos
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) prepareInput(item *Item) []Token {
@@ -357,26 +374,28 @@ 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 algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := int32(part.prefixLength) if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); 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 if pos != nil {
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus for idx := range *pos {
(*pos)[idx] += int(part.prefixLength)
}
}
return Offset{sidx, eidx}, res.Score, int(part.trimLength), pos
} }
} }
// TODO: math.MaxUint16 return Offset{-1, -1}, 0, -1, nil
return Offset{-1, -1, -1}, 0.0
} }

View File

@@ -5,25 +5,32 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
) )
var slab *util.Slab
func init() {
slab = util.MakeSlab(slab16Size, slab32Size)
}
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(true, CaseSmart, terms := parseTerms(true, CaseSmart, false,
"| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |")
if len(terms) != 9 || if len(terms) != 9 ||
terms[0][0].typ != termFuzzy || terms[0][0].inv || terms[0][0].typ != termFuzzy || terms[0][0].inv ||
terms[1][0].typ != termExact || terms[1][0].inv || terms[1][0].typ != termExact || terms[1][0].inv ||
terms[2][0].typ != termPrefix || terms[2][0].inv || terms[2][0].typ != termPrefix || terms[2][0].inv ||
terms[3][0].typ != termSuffix || terms[3][0].inv || terms[3][0].typ != termSuffix || terms[3][0].inv ||
terms[4][0].typ != termFuzzy || !terms[4][0].inv || terms[4][0].typ != termExact || !terms[4][0].inv ||
terms[5][0].typ != termExact || !terms[5][0].inv || terms[5][0].typ != termFuzzy || !terms[5][0].inv ||
terms[6][0].typ != termPrefix || !terms[6][0].inv || terms[6][0].typ != termPrefix || !terms[6][0].inv ||
terms[7][0].typ != termSuffix || !terms[7][0].inv || terms[7][0].typ != termSuffix || !terms[7][0].inv ||
terms[7][1].typ != termEqual || terms[7][1].inv || terms[7][1].typ != termEqual || terms[7][1].inv ||
terms[8][0].typ != termPrefix || terms[8][0].inv || terms[8][0].typ != termPrefix || terms[8][0].inv ||
terms[8][1].typ != termExact || terms[8][1].inv || terms[8][1].typ != termExact || terms[8][1].inv ||
terms[8][2].typ != termSuffix || terms[8][2].inv || terms[8][2].typ != termSuffix || terms[8][2].inv ||
terms[8][3].typ != termFuzzy || !terms[8][3].inv { terms[8][3].typ != termExact || !terms[8][3].inv {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
for idx, termSet := range terms[:8] { for idx, termSet := range terms[:8] {
@@ -43,7 +50,7 @@ func TestParseTermsExtended(t *testing.T) {
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(false, CaseSmart, terms := parseTerms(false, CaseSmart, false,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 ||
@@ -59,7 +66,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -68,26 +75,32 @@ 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, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
res := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
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)
} }
if pos != nil {
t.Errorf("pos is expected to be nil")
}
} }
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, algo.FuzzyMatchV2, true, CaseSmart, false, 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, pos := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
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)
} }
if pos != nil {
t.Errorf("pos is expected to be nil")
}
} }
match("ABC", -1, -1) match("ABC", -1, -1)
match("AbC", 0, 3) match("AbC", 0, 3)
@@ -96,17 +109,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, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, 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 +132,42 @@ 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, algo.FuzzyMatchV2, true, CaseSmart, false, 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, slab) // No cache
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if !(matches[0].item.text.ToString() == "junegunn" &&
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || string(*matches[0].item.origText) == "junegunn.choi" &&
!reflect.DeepEqual(matches[0].transformed, trans) { reflect.DeepEqual(matches[0].item.transformed, trans)) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
match, offsets, pos := pattern.MatchItem(chunk[0], true, slab)
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, offsets, extended)
}
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
t.Error("Invalid pos array", *pos)
}
} }
} }
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, algo.FuzzyMatchV2, extended, CaseSmart, false, 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,14 +34,20 @@ 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.
bytea, err := reader.ReadBytes(delim) bytea, err := reader.ReadBytes(delim)
byteaLen := len(bytea)
if len(bytea) > 0 { if len(bytea) > 0 {
if err == nil { if err == nil {
bytea = bytea[:len(bytea)-1] // get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
bytea = bytea[:byteaLen-2]
} else {
bytea = bytea[:byteaLen-1]
}
} }
if r.pusher(bytea) { if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)

243
src/result.go Normal file
View File

@@ -0,0 +1,243 @@
package fzf
import (
"math"
"sort"
"unicode"
"github.com/junegunn/fzf/src/tui"
"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 tui.ColorPair
attr tui.Attr
index int32
}
type rank struct {
points [4]uint16
index int32
}
type Result struct {
item *Item
rank rank
}
func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item, rank: rank{index: item.index}}
numChars := item.text.Length()
minBegin := math.MaxUint16
maxEnd := 0
validOffsetFound := false
for _, offset := range offsets {
b, e := int(offset[0]), int(offset[1])
if b < e {
minBegin = util.Min(b, minBegin)
maxEnd = util.Max(e, maxEnd)
validOffsetFound = true
}
}
for idx, criterion := range sortCriteria {
val := uint16(math.MaxUint16)
switch criterion {
case byScore:
// Higher is better
val = math.MaxUint16 - util.AsUint16(score)
case byLength:
// If offsets is empty, trimLen will be 0, but we don't care
val = util.AsUint16(trimLen)
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || !unicode.IsSpace(r) {
break
}
}
if criterion == byBegin {
val = util.AsUint16(minBegin - whitePrefixLen)
} else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
}
}
}
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: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
itemColors := result.item.Colors()
// No ANSI code, or --color=no
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
}
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, attr: attr})
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
bg := ansi.color.bg
if theme != nil {
if fg == -1 {
if current {
fg = theme.Current
} else {
fg = theme.Fg
}
}
if bg == -1 {
if current {
bg = theme.DarkBg
} else {
bg = theme.Bg
}
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: tui.NewColorPair(fg, bg),
attr: ansi.color.attr.Merge(attr)})
}
}
}
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 < 4; 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
}

126
src/result_test.go Normal file
View File

@@ -0,0 +1,126 @@
// +build !tcell
package fzf
import (
"math"
"sort"
"testing"
"github.com/junegunn/fzf/src/tui"
"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: [4]uint16{vals[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{byScore, 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-2 || // Bonus
item1.rank.points[1] != 3 || // Length
item1.rank.points[2] != 0 || // Unused
item1.rank.points[3] != 0 || // Unused
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}}, 3, 0)
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4, 0)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5, 0)
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6, 0)
items = []*Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 &&
items[2] == item4 && items[3] == item3 &&
items[4] == item2 && items[5] == item1) {
t.Error(items, item1, item2, item3, item4, item5, item6)
}
}
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, 0}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
pair := tui.NewColorPair(99, 199)
colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
var attr tui.Attr
if bold {
attr = tui.Bold
}
o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
t.Error(o)
}
}
assert(0, 0, 5, tui.NewColorPair(1, 5), false)
assert(1, 5, 15, pair, false)
assert(2, 15, 20, tui.NewColorPair(1, 5), false)
assert(3, 22, 25, tui.NewColorPair(2, 6), true)
assert(4, 25, 35, pair, false)
assert(5, 35, 40, tui.NewColorPair(4, 8), true)
}

File diff suppressed because it is too large Load Diff

73
src/terminal_test.go Normal file
View File

@@ -0,0 +1,73 @@
package fzf
import (
"regexp"
"testing"
"github.com/junegunn/fzf/src/util"
)
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
}
func TestReplacePlaceholder(t *testing.T) {
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")}
items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
var result string
check := func(expected string) {
if result != expected {
t.Errorf("expected: %s, actual: %s", expected, result)
}
}
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar baz'")
// {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
// {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1)
check("echo 'foo'\\''bar baz'")
// {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1)
check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// String delimiter
delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter
regex := regexp.MustCompile("[oa]+")
// foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
}

13
src/terminal_unix.go Normal file
View File

@@ -0,0 +1,13 @@
// +build !windows
package fzf
import (
"os"
"os/signal"
"syscall"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}

11
src/terminal_windows.go Normal file
View File

@@ -0,0 +1,11 @@
// +build windows
package fzf
import (
"os"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
// TODO
}

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)
} }
} }

820
src/tui/light.go Normal file
View File

@@ -0,0 +1,820 @@
package tui
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"golang.org/x/crypto/ssh/terminal"
)
const (
defaultWidth = 80
defaultHeight = 24
escPollInterval = 5
)
const consoleDevice string = "/dev/tty"
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open " + consoleDevice)
}
return in
}
func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true)
}
// FIXME: Need better handling of non-displayable characters
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
bytes := []byte(str)
runes := []rune{}
for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes)
if r == utf8.RuneError || r < 32 &&
r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
runes = append(runes, '?')
} else {
runes = append(runes, r)
}
bytes = bytes[sz:]
}
r.queued += string(runes)
}
func (r *LightRenderer) csi(code string) {
r.stderr("\x1b[" + code)
}
func (r *LightRenderer) flush() {
if len(r.queued) > 0 {
fmt.Fprint(os.Stderr, r.queued)
r.queued = ""
}
}
// Light renderer
type LightRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
prevDownTime time.Time
clickY []int
ttyin *os.File
buffer []byte
origState *terminal.State
width int
height int
yoffset int
tabstop int
escDelay int
upOneLine bool
queued string
y int
x int
maxHeightFunc func(int) int
}
type LightWindow struct {
renderer *LightRenderer
colored bool
border bool
top int
left int
width int
height int
posx int
posy int
tabstop int
bg Color
}
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer {
r := LightRenderer{
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
ttyin: openTtyIn(),
yoffset: -1,
tabstop: tabstop,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
return &r
}
func (r *LightRenderer) fd() int {
return int(r.ttyin.Fd())
}
func (r *LightRenderer) defaultTheme() *ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return Dark256
}
colors, err := exec.Command("tput", "colors").Output()
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
return Dark256
}
return Default16
}
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
bytes := r.getBytesInternal([]byte{})
// ^[[*;*R
if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' {
nums := strings.Split(string(bytes[2:len(bytes)-1]), ";")
if len(nums) == 2 {
return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1
}
return -1, -1
}
// No idea
return -1, -1
}
func repeat(s string, times int) string {
if times > 0 {
return strings.Repeat(s, times)
}
return ""
}
func atoi(s string, defaultValue int) int {
value, err := strconv.Atoi(s)
if err != nil {
return defaultValue
}
return value
}
func (r *LightRenderer) Init() {
delay := 100
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
r.escDelay = delay
fd := r.fd()
origState, err := terminal.GetState(fd)
if err != nil {
errorExit(err.Error())
}
r.origState = origState
terminal.MakeRaw(fd)
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
_, x := r.findOffset()
if x > 0 {
r.upOneLine = true
r.stderr("\n")
}
for i := 1; i < r.MaxY(); i++ {
r.stderr("\n")
r.csi("G")
}
if r.mouse {
r.csi("?1000h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
// r.csi("s")
if r.mouse {
r.yoffset, _ = r.findOffset()
}
}
func (r *LightRenderer) move(y int, x int) {
// w.csi("u")
if r.y < y {
r.csi(fmt.Sprintf("%dB", y-r.y))
} else if r.y > y {
r.csi(fmt.Sprintf("%dA", r.y-y))
}
r.stderr("\r")
if x > 0 {
r.csi(fmt.Sprintf("%dC", x))
}
r.y = y
r.x = x
}
func (r *LightRenderer) origin() {
r.move(0, 0)
}
func getEnv(name string, defaultValue int) int {
env := os.Getenv(name)
if len(env) == 0 {
return defaultValue
}
return atoi(env, defaultValue)
}
func (r *LightRenderer) updateTerminalSize() {
width, height, err := terminal.GetSize(r.fd())
if err == nil {
r.width = width
r.height = r.maxHeightFunc(height)
} else {
r.width = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
}
}
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
b := make([]byte, 1)
util.SetNonblock(r.ttyin, nonblock)
_, err := r.ttyin.Read(b)
if err != nil {
return 0, false
}
return int(b[0]), true
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer)
}
func (r *LightRenderer) getBytesInternal(buffer []byte) []byte {
c, ok := r.getch(false)
if !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
}
retries := 0
if c == ESC {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
for {
c, ok = r.getch(true)
if !ok {
if retries > 0 {
retries--
time.Sleep(escPollInterval * time.Millisecond)
continue
}
break
}
retries = 0
buffer = append(buffer, byte(c))
}
return buffer
}
func (r *LightRenderer) GetChar() Event {
if len(r.buffer) == 0 {
r.buffer = r.getBytes()
}
if len(r.buffer) == 0 {
panic("Empty buffer")
}
sz := 1
defer func() {
r.buffer = r.buffer[sz:]
}()
switch r.buffer[0] {
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
case ESC:
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer = r.getBytes()
ev = r.escSequence(&sz)
}
return ev
}
// CTRL-A ~ CTRL-Z
if r.buffer[0] <= CtrlZ {
return Event{int(r.buffer[0]), 0, nil}
}
char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, char, nil}
}
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{ESC, 0, nil}
}
*sz = 2
switch r.buffer[1] {
case 13:
return Event{AltEnter, 0, nil}
case 32:
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil}
case 98:
return Event{AltB, 0, nil}
case 100:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
return Event{AltBS, 0, nil}
case 91, 79:
if len(r.buffer) < 3 {
return Event{Invalid, 0, nil}
}
*sz = 3
switch r.buffer[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return r.mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(r.buffer) < 4 {
return Event{Invalid, 0, nil}
}
*sz = 4
switch r.buffer[2] {
case 50:
if len(r.buffer) == 5 && r.buffer[4] == 126 {
*sz = 5
switch r.buffer[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
case 51:
return Event{F11, 0, nil}
case 52:
return Event{F12, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch r.buffer[3] {
case 126:
return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(r.buffer) == 5 && r.buffer[4] == 126 {
*sz = 5
switch r.buffer[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:
if len(r.buffer) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch r.buffer[4] {
case 50:
switch r.buffer[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch r.buffer[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // r.buffer[4]
} // r.buffer[3]
} // r.buffer[2]
} // r.buffer[2]
} // r.buffer[1]
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil}
}
func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || r.yoffset < 0 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch r.buffer[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := r.buffer[3] >= 36
down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
double := false
if down {
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
}
r.prevDownTime = now
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
}
return Event{Invalid, 0, nil}
}
func (r *LightRenderer) Pause() {
terminal.Restore(r.fd(), r.origState)
r.csi("?1049h")
r.flush()
}
func (r *LightRenderer) Resume() bool {
terminal.MakeRaw(r.fd())
r.csi("?1049l")
r.flush()
// Should redraw
return true
}
func (r *LightRenderer) Clear() {
// r.csi("u")
r.origin()
r.csi("J")
r.flush()
}
func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush()
}
func (r *LightRenderer) Refresh() {
r.updateTerminalSize()
}
func (r *LightRenderer) Close() {
// r.csi("u")
r.origin()
r.csi("J")
if r.mouse {
r.csi("?1000l")
}
if r.upOneLine {
r.csi("A")
}
r.flush()
terminal.Restore(r.fd(), r.origState)
}
func (r *LightRenderer) MaxX() int {
return r.width
}
func (r *LightRenderer) MaxY() int {
return r.height
}
func (r *LightRenderer) DoesAutoWrap() bool {
return true
}
func (r *LightRenderer) IsOptimized() bool {
return false
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
w := &LightWindow{
renderer: r,
colored: r.theme != nil,
border: border,
top: top,
left: left,
width: width,
height: height,
tabstop: r.tabstop,
bg: colDefault}
if r.theme != nil {
w.bg = r.theme.Bg
}
if w.border {
w.drawBorder()
}
return w
}
func (w *LightWindow) drawBorder() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(ColBorder, AttrRegular, "│")
w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
w.CPrint(ColBorder, AttrRegular, "│")
}
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
}
func (w *LightWindow) csi(code string) {
w.renderer.csi(code)
}
func (w *LightWindow) stderr(str string) {
w.renderer.stderr(str)
}
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR)
}
func (w *LightWindow) Top() int {
return w.top
}
func (w *LightWindow) Left() int {
return w.left
}
func (w *LightWindow) Width() int {
return w.width
}
func (w *LightWindow) Height() int {
return w.height
}
func (w *LightWindow) Refresh() {
}
func (w *LightWindow) Close() {
}
func (w *LightWindow) X() int {
return w.posx
}
func (w *LightWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
}
func (w *LightWindow) Move(y int, x int) {
w.posx = x
w.posy = y
w.renderer.move(w.Top()+y, w.Left()+x)
}
func (w *LightWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
// We should not delete preview window on the right
// csi("K")
w.Print(repeat(" ", w.width-x))
w.Move(y, x)
}
func attrCodes(attr Attr) []string {
codes := []string{}
if (attr & Bold) > 0 {
codes = append(codes, "1")
}
if (attr & Dim) > 0 {
codes = append(codes, "2")
}
if (attr & Italic) > 0 {
codes = append(codes, "3")
}
if (attr & Underline) > 0 {
codes = append(codes, "4")
}
if (attr & Blink) > 0 {
codes = append(codes, "5")
}
if (attr & Reverse) > 0 {
codes = append(codes, "7")
}
return codes
}
func colorCodes(fg Color, bg Color) []string {
codes := []string{}
appendCode := func(c Color, offset int) {
if c == colDefault {
return
}
if c.is24() {
r := (c >> 16) & 0xff
g := (c >> 8) & 0xff
b := (c) & 0xff
codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b))
} else if c >= colBlack && c <= colWhite {
codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset))
} else if c > colWhite && c < 16 {
codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8))
} else if c >= 16 && c < 256 {
codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c))
}
}
appendCode(fg, 0)
appendCode(bg, 10)
return codes
}
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0
}
func (w *LightWindow) Print(text string) {
w.cprint2(colDefault, w.bg, AttrRegular, text)
}
func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
if !w.colored {
w.csiColor(colDefault, colDefault, attrFor(pair, attr))
} else {
w.csiColor(pair.Fg(), pair.Bg(), attr)
}
w.stderrInternal(text, false)
w.csi("m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
if w.csiColor(fg, bg, attr) {
defer w.csi("m")
}
w.stderrInternal(text, false)
}
type wrappedLine struct {
text string
displayWidth int
}
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
lines := []wrappedLine{}
width := 0
line := ""
for _, r := range input {
w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1)
width += w
str := string(r)
if r == '\t' {
str = repeat(" ", w)
}
if prefixLength+width <= max {
line += str
} else {
lines = append(lines, wrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = util.RuneWidth(r, prefixLength, 8)
}
}
lines = append(lines, wrappedLine{string(line), width})
return lines
}
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
allLines := strings.Split(str, "\n")
for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 {
w.MoveAndClear(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth
if j < len(lines)-1 || i < len(allLines)-1 {
if w.posy+1 >= w.height {
return FillSuspend
}
w.MoveAndClear(w.posy+1, 0)
onMove()
}
}
}
return FillContinue
}
func (w *LightWindow) setBg() {
if w.bg != colDefault {
w.csiColor(colDefault, w.bg, AttrRegular)
}
}
func (w *LightWindow) Fill(text string) FillReturn {
w.MoveAndClear(w.posy, w.posx)
w.setBg()
return w.fill(text, w.setBg)
}
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.MoveAndClear(w.posy, w.posx)
if bg == colDefault {
bg = w.bg
}
if w.csiColor(fg, bg, attr) {
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
defer w.csi("m")
}
return w.fill(text, w.setBg)
}
func (w *LightWindow) FinishFill() {
for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0)
}
}
func (w *LightWindow) Erase() {
if w.border {
w.drawBorder()
}
// We don't erase the window here to avoid flickering during scroll
w.Move(0, 0)
}

498
src/tui/ncurses.go Normal file
View File

@@ -0,0 +1,498 @@
// +build !windows
// +build !tcell
package tui
/*
#include <ncurses.h>
#include <locale.h>
#cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
FILE* c_tty() {
return fopen("/dev/tty", "r");
}
SCREEN* c_newterm(FILE* tty) {
return newterm(NULL, stderr, tty);
}
int c_getcurx(WINDOW* win) {
return getcurx(win);
}
*/
import "C"
import (
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
)
type Attr C.uint
type CursesWindow struct {
impl *C.WINDOW
top int
left int
width int
height int
}
func (w *CursesWindow) Top() int {
return w.top
}
func (w *CursesWindow) Left() int {
return w.left
}
func (w *CursesWindow) Width() int {
return w.width
}
func (w *CursesWindow) Height() int {
return w.height
}
func (w *CursesWindow) Refresh() {
C.wnoutrefresh(w.impl)
}
func (w *CursesWindow) FinishFill() {
// NO-OP
}
const (
Bold Attr = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
var Italic Attr = C.A_VERTICAL << 1 // FIXME
const (
AttrRegular Attr = 0
)
var (
_screen *C.SCREEN
_colorMap map[int]int16
_colorFn func(ColorPair, Attr) (C.short, C.int)
)
func init() {
_colorMap = make(map[int]int16)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
}
return Default16
}
func (r *FullscreenRenderer) Init() {
C.setlocale(C.LC_ALL, C.CString(""))
tty := C.c_tty()
if tty == nil {
errorExit("Failed to open /dev/tty")
}
_screen = C.c_newterm(tty)
if _screen == nil {
errorExit("Invalid $TERM: " + os.Getenv("TERM"))
}
C.set_term(_screen)
if r.mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
}
C.noecho()
C.raw() // stty dsusp undef
C.nonl()
C.keypad(C.stdscr, true)
delay := 50
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
C.set_escdelay(C.int(delay))
if r.theme != nil {
C.start_color()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
initPairs(r.theme)
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
_colorFn = attrColored
} else {
initTheme(r.theme, nil, r.forceBlack)
_colorFn = attrMono
}
C.nodelay(C.stdscr, true)
ch := C.getch()
if ch != C.ERR {
C.ungetch(ch)
}
C.nodelay(C.stdscr, false)
}
func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
for _, pair := range []ColorPair{
ColNormal,
ColPrompt,
ColMatch,
ColCurrent,
ColCurrentMatch,
ColSpinner,
ColInfo,
ColCursor,
ColSelected,
ColHeader,
ColBorder} {
C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
}
}
func (r *FullscreenRenderer) Pause() {
C.endwin()
}
func (r *FullscreenRenderer) Resume() bool {
return false
}
func (r *FullscreenRenderer) Close() {
C.endwin()
C.delscreen(_screen)
}
func (r *FullscreenRenderer) 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 r.theme != nil {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
}
if border {
pair, attr := _colorFn(ColBorder, 0)
C.wcolor_set(win, pair, nil)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
C.wcolor_set(win, 0, nil)
}
return &CursesWindow{
impl: win,
top: top,
left: left,
width: width,
height: height,
}
}
func attrColored(color ColorPair, a Attr) (C.short, C.int) {
return C.short(color.index()), C.int(a)
}
func attrMono(color ColorPair, a Attr) (C.short, C.int) {
return 0, C.int(attrFor(color, a))
}
func (r *FullscreenRenderer) MaxX() int {
return int(C.COLS)
}
func (r *FullscreenRenderer) MaxY() int {
return int(C.LINES)
}
func (w *CursesWindow) Close() {
C.delwin(w.impl)
}
func (w *CursesWindow) Enclose(y int, x int) bool {
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
}
func (w *CursesWindow) Move(y int, x int) {
C.wmove(w.impl, C.int(y), C.int(x))
}
func (w *CursesWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.impl)
}
func (w *CursesWindow) Print(text string) {
C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
p, a := _colorFn(color, attr)
C.wcolor_set(w.impl, p, nil)
C.wattron(w.impl, a)
w.Print(text)
C.wattroff(w.impl, a)
C.wcolor_set(w.impl, 0, nil)
}
func (r *FullscreenRenderer) Clear() {
C.clear()
C.endwin()
}
func (r *FullscreenRenderer) Refresh() {
C.refresh()
}
func (w *CursesWindow) Erase() {
C.werase(w.impl)
}
func (w *CursesWindow) X() int {
return int(C.c_getcurx(w.impl))
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return true
}
func (r *FullscreenRenderer) IsOptimized() bool {
return true
}
func (w *CursesWindow) Fill(str string) FillReturn {
if C.waddstr(w.impl, C.CString(str)) == C.OK {
return FillContinue
}
return FillSuspend
}
func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn {
index := ColorPair{fg, bg, -1}.index()
C.wcolor_set(w.impl, C.short(index), nil)
C.wattron(w.impl, C.int(attr))
ret := w.Fill(str)
C.wattroff(w.impl, C.int(attr))
C.wcolor_set(w.impl, 0, nil)
return ret
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
for _, w := range windows {
w.Refresh()
}
C.doupdate()
}
func (p ColorPair) index() int16 {
if p.id >= 0 {
return p.id
}
// ncurses does not support 24-bit colors
if p.is24() {
return ColDefault.index()
}
key := p.key()
if found, prs := _colorMap[key]; prs {
return found
}
id := int16(len(_colorMap)) + ColUser.id
C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
_colorMap[key] = id
return id
}
func consume(expects ...rune) bool {
for _, r := range expects {
if int(C.getch()) != int(r) {
return false
}
}
return true
}
func escSequence() Event {
C.nodelay(C.stdscr, true)
defer func() {
C.nodelay(C.stdscr, false)
}()
c := C.getch()
switch c {
case C.ERR:
return Event{ESC, 0, nil}
case CtrlM:
return Event{AltEnter, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
case ' ':
return Event{AltSpace, 0, nil}
case 127, C.KEY_BACKSPACE:
return Event{AltBS, 0, nil}
case '[':
// Bracketed paste mode (printf "\e[?2004h")
// \e[200~ TEXT \e[201~
if consume('2', '0', '0', '~') {
return Event{Invalid, 0, nil}
}
}
if c >= 'a' && c <= 'z' {
return Event{AltA + int(c) - 'a', 0, nil}
}
if c >= '0' && c <= '9' {
return Event{Alt0 + int(c) - '0', 0, nil}
}
// Don't care. Ignore the rest.
for ; c != C.ERR; c = C.getch() {
}
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) GetChar() Event {
c := C.getch()
switch c {
case C.ERR:
// Unexpected error from blocking read
r.Close()
errorExit("Failed to read /dev/tty")
case C.KEY_UP:
return Event{Up, 0, nil}
case C.KEY_DOWN:
return Event{Down, 0, nil}
case C.KEY_LEFT:
return Event{Left, 0, nil}
case C.KEY_RIGHT:
return Event{Right, 0, nil}
case C.KEY_HOME:
return Event{Home, 0, nil}
case C.KEY_END:
return Event{End, 0, nil}
case C.KEY_BACKSPACE:
return Event{BSpace, 0, nil}
case C.KEY_F0 + 1:
return Event{F1, 0, nil}
case C.KEY_F0 + 2:
return Event{F2, 0, nil}
case C.KEY_F0 + 3:
return Event{F3, 0, nil}
case C.KEY_F0 + 4:
return Event{F4, 0, nil}
case C.KEY_F0 + 5:
return Event{F5, 0, nil}
case C.KEY_F0 + 6:
return Event{F6, 0, nil}
case C.KEY_F0 + 7:
return Event{F7, 0, nil}
case C.KEY_F0 + 8:
return Event{F8, 0, nil}
case C.KEY_F0 + 9:
return Event{F9, 0, nil}
case C.KEY_F0 + 10:
return Event{F10, 0, nil}
case C.KEY_F0 + 11:
return Event{F11, 0, nil}
case C.KEY_F0 + 12:
return Event{F12, 0, nil}
case C.KEY_DC:
return Event{Del, 0, nil}
case C.KEY_PPAGE:
return Event{PgUp, 0, nil}
case C.KEY_NPAGE:
return Event{PgDn, 0, nil}
case C.KEY_BTAB:
return Event{BTab, 0, nil}
case C.KEY_ENTER:
return Event{CtrlM, 0, nil}
case C.KEY_SLEFT:
return Event{SLeft, 0, nil}
case C.KEY_SRIGHT:
return Event{SRight, 0, nil}
case C.KEY_MOUSE:
var me C.MEVENT
if C.getmouse(&me) != C.ERR {
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
x := int(me.x)
y := int(me.y)
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
r.prevDownTime = now
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
}
}
return Event{Invalid, 0, nil}
case C.KEY_RESIZE:
return Event{Resize, 0, nil}
case ESC:
return escSequence()
case 127:
return Event{BSpace, 0, nil}
}
// CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ {
return Event{int(c), 0, nil}
}
// Multi-byte character
buffer := []byte{byte(c)}
for {
r, _ := utf8.DecodeRune(buffer)
if r != utf8.RuneError {
return Event{Rune, r, nil}
}
c := C.getch()
if c == C.ERR {
break
}
if c >= C.KEY_CODE_YES {
C.ungetch(c)
break
}
buffer = append(buffer, byte(c))
}
return Event{Invalid, 0, nil}
}

560
src/tui/tcell.go Normal file
View File

@@ -0,0 +1,560 @@
// +build tcell windows
package tui
import (
"time"
"unicode/utf8"
"runtime"
// https://github.com/gdamore/tcell/pull/135
"github.com/junegunn/tcell"
"github.com/junegunn/tcell/encoding"
"github.com/junegunn/go-runewidth"
)
func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
}
type Attr tcell.Style
type TcellWindow struct {
color bool
top int
left int
width int
height int
lastX int
lastY int
moveCursor bool
border bool
}
func (w *TcellWindow) Top() int {
return w.top
}
func (w *TcellWindow) Left() int {
return w.left
}
func (w *TcellWindow) Width() int {
return w.width
}
func (w *TcellWindow) Height() int {
return w.height
}
func (w *TcellWindow) Refresh() {
if w.moveCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
w.moveCursor = false
}
w.lastX = 0
w.lastY = 0
if w.border {
w.drawBorder()
}
}
func (w *TcellWindow) FinishFill() {
// NO-OP
}
const (
Bold Attr = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
Italic = Attr(tcell.AttrNone) // Not supported
)
const (
AttrRegular Attr = 0
)
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
return Dark256
}
return Default16
}
var (
_colorToAttribute = []tcell.Color{
tcell.ColorBlack,
tcell.ColorRed,
tcell.ColorGreen,
tcell.ColorYellow,
tcell.ColorBlue,
tcell.ColorDarkMagenta,
tcell.ColorLightCyan,
tcell.ColorWhite,
}
)
func (c Color) Style() tcell.Color {
if c <= colDefault {
return tcell.ColorDefault
} else if c >= colBlack && c <= colWhite {
return _colorToAttribute[int(c)]
} else {
return tcell.Color(c)
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
var (
_screen tcell.Screen
)
func (r *FullscreenRenderer) initScreen() {
s, e := tcell.NewScreen()
if e != nil {
errorExit(e.Error())
}
if e = s.Init(); e != nil {
errorExit(e.Error())
}
if r.mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
}
func (r *FullscreenRenderer) Init() {
encoding.Register()
r.initScreen()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
}
func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size()
return int(ncols)
}
func (r *FullscreenRenderer) MaxY() int {
_, nlines := _screen.Size()
return int(nlines)
}
func (w *TcellWindow) X() int {
return w.lastX
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false
}
func (r *FullscreenRenderer) IsOptimized() bool {
return false
}
func (r *FullscreenRenderer) Clear() {
_screen.Sync()
_screen.Clear()
}
func (r *FullscreenRenderer) Refresh() {
// noop
}
func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
return Event{Resize, 0, nil}
// process mouse events:
case *tcell.EventMouse:
x, y := ev.Position()
button := ev.Buttons()
mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}}
} else if runtime.GOOS != "windows" {
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
// to consume them.
down := button&tcell.Button1 != 0 // left
double := false
if down {
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x)
} else {
r.clickY = []int{x}
r.prevDownTime = now
}
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
}
// process keyboard:
case *tcell.EventKey:
alt := (ev.Modifiers() & tcell.ModAlt) > 0
switch ev.Key() {
case tcell.KeyCtrlA:
return Event{CtrlA, 0, nil}
case tcell.KeyCtrlB:
return Event{CtrlB, 0, nil}
case tcell.KeyCtrlC:
return Event{CtrlC, 0, nil}
case tcell.KeyCtrlD:
return Event{CtrlD, 0, nil}
case tcell.KeyCtrlE:
return Event{CtrlE, 0, nil}
case tcell.KeyCtrlF:
return Event{CtrlF, 0, nil}
case tcell.KeyCtrlG:
return Event{CtrlG, 0, nil}
case tcell.KeyCtrlJ:
return Event{CtrlJ, 0, nil}
case tcell.KeyCtrlK:
return Event{CtrlK, 0, nil}
case tcell.KeyCtrlL:
return Event{CtrlL, 0, nil}
case tcell.KeyCtrlM:
if alt {
return Event{AltEnter, 0, nil}
}
return Event{CtrlM, 0, nil}
case tcell.KeyCtrlN:
return Event{CtrlN, 0, nil}
case tcell.KeyCtrlO:
return Event{CtrlO, 0, nil}
case tcell.KeyCtrlP:
return Event{CtrlP, 0, nil}
case tcell.KeyCtrlQ:
return Event{CtrlQ, 0, nil}
case tcell.KeyCtrlR:
return Event{CtrlR, 0, nil}
case tcell.KeyCtrlS:
return Event{CtrlS, 0, nil}
case tcell.KeyCtrlT:
return Event{CtrlT, 0, nil}
case tcell.KeyCtrlU:
return Event{CtrlU, 0, nil}
case tcell.KeyCtrlV:
return Event{CtrlV, 0, nil}
case tcell.KeyCtrlW:
return Event{CtrlW, 0, nil}
case tcell.KeyCtrlX:
return Event{CtrlX, 0, nil}
case tcell.KeyCtrlY:
return Event{CtrlY, 0, nil}
case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
}
return Event{BSpace, 0, nil}
case tcell.KeyUp:
return Event{Up, 0, nil}
case tcell.KeyDown:
return Event{Down, 0, nil}
case tcell.KeyLeft:
return Event{Left, 0, nil}
case tcell.KeyRight:
return Event{Right, 0, nil}
case tcell.KeyHome:
return Event{Home, 0, nil}
case tcell.KeyDelete:
return Event{Del, 0, nil}
case tcell.KeyEnd:
return Event{End, 0, nil}
case tcell.KeyPgUp:
return Event{PgUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
case tcell.KeyTab:
return Event{Tab, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
return Event{F2, 0, nil}
case tcell.KeyF3:
return Event{F3, 0, nil}
case tcell.KeyF4:
return Event{F4, 0, nil}
case tcell.KeyF5:
return Event{F5, 0, nil}
case tcell.KeyF6:
return Event{F6, 0, nil}
case tcell.KeyF7:
return Event{F7, 0, nil}
case tcell.KeyF8:
return Event{F8, 0, nil}
case tcell.KeyF9:
return Event{F9, 0, nil}
case tcell.KeyF10:
return Event{F10, 0, nil}
case tcell.KeyF11:
return Event{F11, 0, nil}
case tcell.KeyF12:
return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space:
case tcell.KeyRune:
r := ev.Rune()
if alt {
switch r {
case ' ':
return Event{AltSpace, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
}
if r >= 'a' && r <= 'z' {
return Event{AltA + int(r) - 'a', 0, nil}
}
if r >= '0' && r <= '9' {
return Event{Alt0 + int(r) - '0', 0, nil}
}
}
return Event{Rune, r, nil}
case tcell.KeyEsc:
return Event{ESC, 0, nil}
}
}
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) Pause() {
_screen.Fini()
}
func (r *FullscreenRenderer) Resume() bool {
r.initScreen()
return true
}
func (r *FullscreenRenderer) Close() {
_screen.Fini()
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
// TODO
for _, w := range windows {
w.Refresh()
}
_screen.Show()
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
// TODO
return &TcellWindow{
color: r.theme != nil,
top: top,
left: left,
width: width,
height: height,
border: border}
}
func (w *TcellWindow) Close() {
// TODO
}
func fill(x, y, w, h int, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style())
}
}
}
func (w *TcellWindow) Erase() {
// TODO
fill(w.left, w.top, w.width, w.height, ' ')
}
func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
}
func (w *TcellWindow) Move(y int, x int) {
w.lastX = x
w.lastY = y
w.moveCursor = true
}
func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style())
}
w.lastX = x
}
func (w *TcellWindow) Print(text string) {
w.printString(text, ColDefault, 0)
}
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
t := text
lx := 0
var style tcell.Style
if w.color {
style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
} else {
style = ColDefault.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
for {
if len(t) == 0 {
break
}
r, size := utf8.DecodeRuneInString(t)
t = t[size:]
if r < rune(' ') { // ignore control characters
continue
}
if r == '\n' {
w.lastY++
lx = 0
} else {
if r == '\u000D' { // skip carriage return
continue
}
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
}
}
w.lastX += lx
}
func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
w.printString(text, pair, attr)
}
func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn {
lx := 0
var style tcell.Style
if w.color {
style = pair.style()
} else {
style = ColDefault.style()
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
for _, r := range text {
if r == '\n' {
w.lastY++
w.lastX = 0
lx = 0
} else {
var xPos = w.left + w.lastX + lx
// word wrap:
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
var yPos = w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
}
}
w.lastX += lx
return FillContinue
}
func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, ColDefault, 0)
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, ColorPair{fg, bg, -1}, a)
}
func (w *TcellWindow) drawBorder() {
left := w.left
right := left + w.width
top := w.top
bot := top + w.height
var style tcell.Style
if w.color {
style = ColBorder.style()
} else {
style = ColDefault.style()
}
for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
}
for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
}

416
src/tui/tui.go Normal file
View File

@@ -0,0 +1,416 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
)
// Types of user action
const (
Rune = iota
CtrlA
CtrlB
CtrlC
CtrlD
CtrlE
CtrlF
CtrlG
CtrlH
Tab
CtrlJ
CtrlK
CtrlL
CtrlM
CtrlN
CtrlO
CtrlP
CtrlQ
CtrlR
CtrlS
CtrlT
CtrlU
CtrlV
CtrlW
CtrlX
CtrlY
CtrlZ
ESC
Invalid
Resize
Mouse
DoubleClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
AltEnter
AltSpace
AltSlash
AltBS
Alt0
)
const ( // Reset iota
AltA = Alt0 + 'a' - '0' + iota
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
const (
doubleClickDuration = 500 * time.Millisecond
)
type Color int32
func (c Color) is24() bool {
return c > 0 && (c&(1<<24)) > 0
}
const (
colUndefined Color = -2
colDefault = -1
)
const (
colBlack Color = iota
colRed
colGreen
colYellow
colBlue
colMagenta
colCyan
colWhite
)
type FillReturn int
const (
FillContinue FillReturn = iota
FillNextLine
FillSuspend
)
type ColorPair struct {
fg Color
bg Color
id int16
}
func HexToColor(rrggbb string) Color {
r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0)
g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0)
b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0)
return Color((1 << 24) + (r << 16) + (g << 8) + b)
}
func NewColorPair(fg Color, bg Color) ColorPair {
return ColorPair{fg, bg, -1}
}
func (p ColorPair) Fg() Color {
return p.fg
}
func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) key() int {
return (int(p.Fg()) << 8) + int(p.Bg())
}
func (p ColorPair) is24() bool {
return p.Fg().is24() || p.Bg().is24()
}
type ColorTheme struct {
Fg Color
Bg Color
DarkBg Color
Prompt Color
Match Color
Current Color
CurrentMatch Color
Spinner Color
Info Color
Cursor Color
Selected Color
Header Color
Border Color
}
func (t *ColorTheme) HasBg() bool {
return t.Bg != colDefault
}
type Event struct {
Type int
Char rune
MouseEvent *MouseEvent
}
type MouseEvent struct {
Y int
X int
S int
Down bool
Double bool
Mod bool
}
type Renderer interface {
Init()
Pause()
Resume() bool
Clear()
RefreshWindows(windows []Window)
Refresh()
Close()
GetChar() Event
MaxX() int
MaxY() int
DoesAutoWrap() bool
IsOptimized() bool
NewWindow(top int, left int, width int, height int, border bool) Window
}
type Window interface {
Top() int
Left() int
Width() int
Height() int
Refresh()
FinishFill()
Close()
X() int
Enclose(y int, x int) bool
Move(y int, x int)
MoveAndClear(y int, x int)
Print(text string)
CPrint(color ColorPair, attr Attr, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
Erase()
}
type FullscreenRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
prevDownTime time.Time
clickY []int
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
r := &FullscreenRenderer{
theme: theme,
mouse: mouse,
forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0),
clickY: []int{}}
return r
}
var (
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
ColDefault ColorPair
ColNormal ColorPair
ColPrompt ColorPair
ColMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColCursor ColorPair
ColSelected ColorPair
ColHeader ColorPair
ColBorder ColorPair
ColUser ColorPair
)
func EmptyTheme() *ColorTheme {
return &ColorTheme{
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 errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() {
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: colBlack,
Prompt: colBlue,
Match: colGreen,
Current: colYellow,
CurrentMatch: colGreen,
Spinner: colGreen,
Info: colWhite,
Cursor: colRed,
Selected: colMagenta,
Header: colCyan,
Border: colBlack}
Dark256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109,
Border: 59}
Light256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
}
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
if theme == nil {
initPalette(theme)
return
}
if forceBlack {
theme.Bg = colBlack
}
o := func(a Color, b Color) Color {
if b == colUndefined {
return a
}
return b
}
theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
initPalette(theme)
}
func initPalette(theme *ColorTheme) {
ColDefault = ColorPair{colDefault, colDefault, 0}
if theme != nil {
ColNormal = ColorPair{theme.Fg, theme.Bg, 1}
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2}
ColMatch = ColorPair{theme.Match, theme.Bg, 3}
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4}
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5}
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6}
ColInfo = ColorPair{theme.Info, theme.Bg, 7}
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8}
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9}
ColHeader = ColorPair{theme.Header, theme.Bg, 10}
ColBorder = ColorPair{theme.Border, theme.Bg, 11}
} else {
ColNormal = ColorPair{colDefault, colDefault, 1}
ColPrompt = ColorPair{colDefault, colDefault, 2}
ColMatch = ColorPair{colDefault, colDefault, 3}
ColCurrent = ColorPair{colDefault, colDefault, 4}
ColCurrentMatch = ColorPair{colDefault, colDefault, 5}
ColSpinner = ColorPair{colDefault, colDefault, 6}
ColInfo = ColorPair{colDefault, colDefault, 7}
ColCursor = ColorPair{colDefault, colDefault, 8}
ColSelected = ColorPair{colDefault, colDefault, 9}
ColHeader = ColorPair{colDefault, colDefault, 10}
ColBorder = ColorPair{colDefault, colDefault, 11}
}
ColUser = ColorPair{colDefault, colDefault, 12}
}
func attrFor(color ColorPair, attr Attr) Attr {
switch color {
case ColCurrent:
return attr | Reverse
case ColMatch:
return attr | Underline
case ColCurrentMatch:
return attr | Underline | Reverse
}
return attr
}

20
src/tui/tui_test.go Normal file
View File

@@ -0,0 +1,20 @@
package tui
import "testing"
func TestHexToColor(t *testing.T) {
assert := func(expr string, r, g, b int) {
color := HexToColor(expr)
if !color.is24() ||
int((color>>16)&0xff) != r ||
int((color>>8)&0xff) != g ||
int((color)&0xff) != b {
t.Fail()
}
}
assert("#ff0000", 255, 0, 0)
assert("#010203", 1, 2, 3)
assert("#102030", 16, 32, 48)
assert("#ffffff", 255, 255, 255)
}

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...>"

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

@@ -0,0 +1,157 @@
package util
import (
"unicode"
"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 !unicode.IsSpace(char) {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if !unicode.IsSpace(char) {
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 !unicode.IsSpace(char) {
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", "")
}

12
src/util/slab.go Normal file
View File

@@ -0,0 +1,12 @@
package util
type Slab struct {
I16 []int16
I32 []int32
}
func MakeSlab(size16 int, size32 int) *Slab {
return &Slab{
I16: make([]int16, size16),
I32: make([]int32, size32)}
}

View File

@@ -1,24 +1,51 @@
package util package util
// #include <unistd.h>
import "C"
import ( import (
"math"
"os" "os"
"os/exec"
"time" "time"
"unicode/utf8"
"github.com/junegunn/go-isatty"
"github.com/junegunn/go-runewidth"
) )
// Max returns the largest integer var _runeWidths = make(map[rune]int)
func Max(first int, items ...int) int {
max := first // RuneWidth returns rune width
for _, item := range items { func RuneWidth(r rune, prefixWidth int, tabstop int) int {
if item > max { if r == '\t' {
max = item return tabstop - prefixWidth%tabstop
} } else if w, found := _runeWidths[r]; found {
return w
} else {
w := Max(runewidth.RuneWidth(r), 1)
_runeWidths[r] = w
return w
} }
return max }
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {
return first
}
return second
}
// Max16 returns the largest integer
func Max16(first int16, second int16) int16 {
if first >= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
} }
// Min returns the smallest integer // Min returns the smallest integer
@@ -37,14 +64,6 @@ func Min32(first int32, second int32) int32 {
return second return second
} }
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
}
// Constrain32 limits the given 32-bit integer with the upper and lower bounds // Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 { func Constrain32(val int32, min int32, max int32) int32 {
if val < min { if val < min {
@@ -67,6 +86,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 {
@@ -81,66 +109,5 @@ func DurWithin(
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return isatty.IsTerminal(os.Stdin.Fd())
}
// 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
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
} }

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)
}

28
src/util/util_unix.go Normal file
View File

@@ -0,0 +1,28 @@
// +build !windows
package util
import (
"os"
"os/exec"
"syscall"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return false
}
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(int(file.Fd()), nonblock)
}

34
src/util/util_windows.go Normal file
View File

@@ -0,0 +1,34 @@
// +build windows
package util
import (
"os"
"os/exec"
"syscall"
"github.com/junegunn/go-shellwords"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
args, _ := shellwords.Parse(command)
allArgs := make([]string, len(args)+1)
allArgs[0] = "/c"
copy(allArgs[1:], args)
return exec.Command(shell, allArgs...)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return true
}
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
}

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,83 @@ 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'
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
let opts = fzf#wrap({})
Assert opts.options =~ '^--color=fg:'
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
@@ -105,8 +117,28 @@ class Tmux
wait do wait do
lines = capture(pane) lines = capture(pane)
class << lines class << lines
def counts
self.lazy
.map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ }
.reject(&:empty?)
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
end
def match_count
counts[0]
end
def item_count def item_count
self[-2] ? self[-2].strip.split('/').last.to_i : 0 counts[1]
end
def select_count
counts[2]
end
def any_include? val
method = val.is_a?(Regexp) ? :match : :include?
self.select { |line| line.send method, val }.first
end end
end end
yield lines yield lines
@@ -124,8 +156,10 @@ class Tmux
def prepare def prepare
tries = 0 tries = 0
begin begin
self.send_keys 'C-u', 'hello', 'Right' self.until do |lines|
self.until { |lines| lines[-1].end_with?('hello') } self.send_keys 'C-u', 'hello'
lines[-1].end_with?('hello')
end
rescue Exception rescue Exception
(tries += 1) < 5 ? retry : raise (tries += 1) < 5 ? retry : raise
end end
@@ -149,10 +183,9 @@ class TestBase < Minitest::Test
@temp_suffix].join '-' @temp_suffix].join '-'
end end
def setup def writelines path, lines
ENV.delete 'FZF_DEFAULT_OPTS' File.unlink path while File.exists? path
ENV.delete 'FZF_CTRL_T_COMMAND' File.open(path, 'w') { |f| f << lines.join($/) + $/ }
ENV.delete 'FZF_DEFAULT_COMMAND'
end end
def readonce def readonce
@@ -289,6 +322,19 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines.last !~ /^>/ } tmux.until { |lines| lines.last !~ /^>/ }
end end
def test_file_word
tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Escape, :b
tmux.send_keys :Escape, :b
tmux.send_keys :Escape, :b
tmux.send_keys :Escape, :d
tmux.send_keys :Escape, :f
tmux.send_keys :Escape, :BSpace
tmux.until { |lines| lines.last == '> --///baz' }
end
def test_multi_order def test_multi_order
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
@@ -362,7 +408,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($/)
@@ -446,6 +492,15 @@ class TestGoFZF < TestBase
assert_equal ['55', 'alt-z', '55'], readonce.split($/) assert_equal ['55', 'alt-z', '55'], readonce.split($/)
end end
def test_expect_printable_character_print_query
tmux.send_keys "seq 1 100 | #{fzf '--expect=z --print-query'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys 'z'
assert_equal ['55', 'z', '55'], readonce.split($/)
end
def test_expect_print_query_select_1 def test_expect_print_query_select_1
tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter
assert_equal ['55', '', '55'], readonce.split($/) assert_equal ['55', '', '55'], readonce.split($/)
@@ -511,162 +566,91 @@ class TestGoFZF < TestBase
assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/) assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/)
end end
# Since 0.11.2 def test_tiebreak_index_begin
def test_tiebreak_list writelines tempname, [
input = %w[ 'xoxxxxxoxx',
f-o-o-b-a-r 'xoxxxxxox',
foobar---- 'xxoxxxoxx',
--foobar 'xxxoxoxxx',
----foobar 'xxxxoxox',
foobar-- ' xxoxoxxx',
--foobar--
foobar
] ]
writelines tempname, input
assert_equal %w[ assert_equal [
foobar---- 'xxxxoxox',
--foobar ' xxoxoxxx',
----foobar 'xxxoxoxxx',
foobar-- 'xxoxxxoxx',
--foobar-- 'xoxxxxxox',
foobar 'xoxxxxxoxx',
f-o-o-b-a-r ], `#{FZF} -foo < #{tempname}`.split($/)
], `#{FZF} -ffb --tiebreak=index < #{tempname}`.split($/)
by_length = %w[ assert_equal [
foobar 'xxxoxoxxx',
--foobar 'xxxxoxox',
foobar-- ' xxoxoxxx',
foobar---- 'xxoxxxoxx',
----foobar 'xoxxxxxoxx',
--foobar-- 'xoxxxxxox',
f-o-o-b-a-r ], `#{FZF} -foo --tiebreak=index < #{tempname}`.split($/)
]
assert_equal by_length, `#{FZF} -ffb < #{tempname}`.split($/)
assert_equal by_length, `#{FZF} -ffb --tiebreak=length < #{tempname}`.split($/)
assert_equal %w[ # Note that --tiebreak=begin is now based on the first occurrence of the
foobar # first character on the pattern
foobar-- assert_equal [
--foobar ' xxoxoxxx',
foobar---- 'xxxoxoxxx',
--foobar-- 'xxxxoxox',
----foobar 'xxoxxxoxx',
f-o-o-b-a-r 'xoxxxxxoxx',
], `#{FZF} -ffb --tiebreak=length,begin < #{tempname}`.split($/) 'xoxxxxxox',
], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($/)
assert_equal %w[ assert_equal [
foobar ' xxoxoxxx',
--foobar 'xxxoxoxxx',
foobar-- 'xxxxoxox',
----foobar 'xxoxxxoxx',
--foobar-- 'xoxxxxxox',
foobar---- 'xoxxxxxoxx',
f-o-o-b-a-r ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/)
], `#{FZF} -ffb --tiebreak=length,end < #{tempname}`.split($/)
assert_equal %w[
foobar----
foobar--
foobar
--foobar
--foobar--
----foobar
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/)
by_begin_end = %w[
foobar
foobar--
foobar----
--foobar
--foobar--
----foobar
f-o-o-b-a-r
]
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/)
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,end < #{tempname}`.split($/)
assert_equal %w[
--foobar
----foobar
foobar
foobar--
--foobar--
foobar----
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=end < #{tempname}`.split($/)
by_begin_end = %w[
foobar
--foobar
----foobar
foobar--
--foobar--
foobar----
f-o-o-b-a-r
]
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,begin < #{tempname}`.split($/)
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/)
end end
def test_tiebreak_white_prefix def test_tiebreak_end
writelines tempname, [ writelines tempname, [
'f o o b a r', 'xoxxxxxxxx',
' foo bar', 'xxoxxxxxxx',
' foobar', 'xxxoxxxxxx',
'----foo bar', 'xxxxoxxxx',
'----foobar', 'xxxxxoxxx',
' foo bar', ' xxxxoxxx',
' foobar--',
' foobar',
'--foo bar',
'--foobar',
'foobar',
] ]
assert_equal [ assert_equal [
' foobar', ' xxxxoxxx',
' foobar', 'xxxxoxxxx',
'foobar', 'xxxxxoxxx',
' foobar--', 'xoxxxxxxxx',
'--foobar', 'xxoxxxxxxx',
'----foobar', 'xxxoxxxxxx',
' foo bar', ], `#{FZF} -fo < #{tempname}`.split($/)
' foo bar',
'--foo bar',
'----foo bar',
'f o o b a r',
], `#{FZF} -ffb < #{tempname}`.split($/)
assert_equal [ assert_equal [
' foobar', 'xxxxxoxxx',
' foobar--', ' xxxxoxxx',
' foobar', 'xxxxoxxxx',
'foobar', 'xxxoxxxxxx',
'--foobar', 'xxoxxxxxxx',
'----foobar', 'xoxxxxxxxx',
' foo bar', ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/)
' foo bar',
'--foo bar',
'----foo bar',
'f o o b a r',
], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/)
assert_equal [ assert_equal [
' foobar', 'xxxxxoxxx',
' foobar', ' xxxxoxxx',
'foobar', 'xxxxoxxxx',
' foobar--', 'xxxoxxxxxx',
'--foobar', 'xxoxxxxxxx',
'----foobar', 'xoxxxxxxxx',
' foo bar', ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($/)
' foo bar',
'--foo bar',
'----foo bar',
'f o o b a r',
], `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/)
end end
def test_tiebreak_length_with_nth def test_tiebreak_length_with_nth
@@ -742,17 +726,6 @@ class TestGoFZF < TestBase
assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/) assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/)
end end
def test_tiebreak_end_backward_scan
input = %w[
foobar-fb
fubar
]
writelines tempname, input
assert_equal input.reverse, `#{FZF} -f fb < #{tempname}`.split($/)
assert_equal input, `#{FZF} -f fb --tiebreak=end < #{tempname}`.split($/)
end
def test_invalid_cache def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' } tmux.until { |lines| lines[-2].include? '2/3' }
@@ -1121,9 +1094,9 @@ class TestGoFZF < TestBase
end end
def test_invalid_term def test_invalid_term
lines = `TERM=xxx #{FZF}` lines = `TERM=xxx #{FZF} 2>&1`
assert_equal 2, $?.exitstatus assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx') assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
end end
def test_invalid_option def test_invalid_option
@@ -1229,14 +1202,20 @@ class TestGoFZF < TestBase
end end
def test_preview def test_preview
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --bind ?:toggle-preview], :Enter 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.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') }
tmux.send_keys '555' tmux.send_keys '555'
tmux.until { |lines| lines[1].include?(' {555-555}') } tmux.until { |lines| lines[1].include?(' {555-555}') }
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| !lines[1].include?(' {555-555}') } tmux.until { |lines| !lines[1].include?(' {555-555}') }
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[1].include?(' {555-555}') } 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 end
def test_preview_hidden def test_preview_hidden
@@ -1249,12 +1228,6 @@ class TestGoFZF < TestBase
tmux.send_keys '?' tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' } tmux.until { |lines| lines[-1] == '> 555' }
end end
private
def writelines path, lines
File.unlink path while File.exists? path
File.open(path, 'w') { |f| f << lines.join($/) + $/ }
end
end end
module TestShell module TestShell
@@ -1272,61 +1245,66 @@ module TestShell
tmux.prepare tmux.prepare
end end
def test_ctrl_t def unset_var name
tmux.prepare
tmux.send_keys "unset #{name}", :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c'
# FZF_TMUX=0
new_shell
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, pane: 0
tmux.until(0) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 0
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d'
end end
def test_ctrl_t_command def test_ctrl_t
set_var "FZF_CTRL_T_COMMAND", "seq 100" set_var "FZF_CTRL_T_COMMAND", "seq 100"
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count == 100 } lines = retries do
tmux.send_keys :BTab, :BTab, :BTab, pane: 1 tmux.prepare
tmux.until(1) { |lines| lines[-2].include?('(3)') } tmux.send_keys 'C-t'
tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines.item_count == 100 }
tmux.until(0) { |lines| lines[-1].include? '1 2 3' } end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| lines.any_include? ' (3)' }
tmux.send_keys :Enter
tmux.until { |lines| lines.any_include? '1 2 3' }
tmux.send_keys 'C-c'
end end
def test_ctrl_t_unicode def test_ctrl_t_unicode
FileUtils.mkdir_p '/tmp/fzf-test' writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']
tmux.send_keys 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"', :Enter set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}"
tmux.prepare
tmux.send_keys 'cat ', 'C-t', pane: 0 retries do
tmux.until(1) { |lines| lines.item_count >= 1 } tmux.prepare
tmux.send_keys 'fzf-unicode', pane: 1 tmux.send_keys 'echo ', 'C-t'
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until { |lines| lines.item_count == 2 }
tmux.send_keys :BTab, :BTab, pane: 1 end
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.send_keys 'fzf-unicode'
tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines.match_count == 2 }
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.send_keys '1'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Tab
tmux.until { |lines| lines.select_count == 1 }
tmux.send_keys :BSpace
tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys '2'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Tab
tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' } tmux.until { |lines| lines.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ }
tmux.send_keys :Enter
tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ }
end end
def test_alt_c def test_alt_c
tmux.prepare lines = retries do
tmux.send_keys :Escape, :c, pane: 0 tmux.prepare
lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] } tmux.send_keys :Escape, :c
expected = lines[-3][2..-1] tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter, pane: 1 end
expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1]
tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
tmux.until { |lines| lines[-1].end_with?(expected) } tmux.until { |lines| lines[-1].end_with?(expected) }
@@ -1338,10 +1316,12 @@ module TestShell
tmux.prepare tmux.prepare
tmux.send_keys 'cd /', :Enter tmux.send_keys 'cd /', :Enter
tmux.prepare retries do
tmux.send_keys :Escape, :c, pane: 0 tmux.prepare
lines = tmux.until(1) { |lines| lines.item_count == 1 } tmux.send_keys :Escape, :c
tmux.send_keys :Enter, pane: 1 lines = tmux.until { |lines| lines.item_count == 1 }
end
tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
@@ -1354,16 +1334,29 @@ module TestShell
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter
tmux.send_keys 'C-r', pane: 0 retries do
tmux.until(1) { |lines| lines.item_count > 0 } tmux.prepare
tmux.send_keys '3d', pane: 1 tmux.send_keys 'C-r'
tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter, pane: 1 end
tmux.send_keys '3d'
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' } tmux.until { |lines| lines[-1] == '3rd' }
end end
def retries times = 3, &block
(times - 1).times do |t|
begin
return block.call
rescue RuntimeError
end
end
block.call
end
end end
module CompletionTest module CompletionTest
@@ -1375,12 +1368,12 @@ module CompletionTest
FileUtils.touch File.expand_path(f) FileUtils.touch File.expand_path(f)
end end
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys ' !d' tmux.send_keys ' !d'
tmux.until(1) { |lines| lines[-2].include?(' 2/') } tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys :BTab, :BTab tmux.send_keys :Tab, :Tab
tmux.until(1) { |lines| lines[-2].include?('(2)') } tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -1390,10 +1383,10 @@ module CompletionTest
# ~USERNAME**<TAB> # ~USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 tmux.send_keys "cat ~#{ENV['USER']}**", :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys '.fzf-home' tmux.send_keys "'.fzf-home"
tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' } tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -1402,8 +1395,8 @@ module CompletionTest
# ~INVALID_USERNAME**<TAB> # ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab, pane: 0 tmux.send_keys "cat ~such**", :Tab
tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' } tmux.until { |lines| lines.any_include? 'no~such~user' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -1412,9 +1405,11 @@ module CompletionTest
# /tmp/fzf\ test**<TAB> # /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'foobar$'
tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar') lines[-1].end_with?('/tmp/fzf\ test/foobar')
@@ -1423,11 +1418,10 @@ module CompletionTest
# Should include hidden files # Should include hidden files
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
tmux.send_keys 'C-u' tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
tmux.until(1) do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-2].include?('100/') && lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
lines[-3].include?('/tmp/fzf-test/.hidden-')
end end
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
@@ -1437,19 +1431,22 @@ module CompletionTest
end end
def test_file_completion_root def test_file_completion_root
tmux.send_keys 'ls /**', :Tab, pane: 0 tmux.send_keys 'ls /**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_dir_completion def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter (1..100).each do |idx|
FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}"
end
FileUtils.touch '/tmp/fzf-test/d55/xxx'
tmux.prepare tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -1476,14 +1473,15 @@ module CompletionTest
lines = tmux.until { |lines| lines[-1].start_with? '[1]' } lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last pid = lines[-1].split.last
tmux.prepare tmux.prepare
tmux.send_keys 'kill ', :Tab, pane: 0 tmux.send_keys 'C-L'
tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys 'kill ', :Tab
tmux.until { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' } tmux.until { |lines| lines.any_include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}" lines[-1].include? "kill #{pid}"
end end
ensure ensure
Process.kill 'KILL', pid.to_i rescue nil if pid Process.kill 'KILL', pid.to_i rescue nil if pid
@@ -1492,10 +1490,10 @@ module CompletionTest
def test_custom_completion def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 tmux.send_keys 'ls /tmp/**', :Tab
tmux.until(1) { |lines| lines.item_count == 11 } tmux.until { |lines| lines.item_count == 11 }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :Tab, :Tab, :Tab
tmux.until(1) { |lines| lines[-2].include? '(3)' } tmux.until { |lines| lines.select_count == 3 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -1504,34 +1502,49 @@ module CompletionTest
end end
def test_unset_completion def test_unset_completion
tmux.send_keys 'export FOO=BAR', :Enter tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare tmux.prepare
# Using tmux # Using tmux
tmux.send_keys 'unset FOO**', :Tab, pane: 0 tmux.send_keys 'unset FZFFOO**', :Tab
tmux.until(1) { |lines| lines[-2].include? ' 1/' } tmux.until { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
# FZF_TMUX=0 # FZF_TMUX=1
new_shell new_shell
tmux.send_keys 'unset FOO**', :Tab tmux.send_keys 'unset FZFFO**', :Tab, pane: 0
tmux.until { |lines| lines[-2].include? ' 1/' } tmux.until(1) { |lines| lines.match_count == 1 }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' } tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
end end
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
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.send_keys '1'
tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines.match_count == 1 }
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.send_keys :Tab
tmux.until { |lines| lines.select_count == 1 }
tmux.send_keys :BSpace
tmux.until { |lines| lines.match_count == 2 }
tmux.send_keys '2'
tmux.until { |lines| lines.select_count == 1 }
tmux.send_keys :Tab
tmux.until { |lines| lines.select_count == 2 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-l'
lines.any_include? 'cat'
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test3test4' } tmux.until { |lines| lines[-1].include? 'test3test4' }
end end
@@ -1543,7 +1556,7 @@ class TestBash < TestBase
def new_shell def new_shell
tmux.prepare tmux.prepare
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
tmux.prepare tmux.prepare
end end
@@ -1558,7 +1571,7 @@ class TestZsh < TestBase
include CompletionTest include CompletionTest
def new_shell def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
tmux.prepare tmux.prepare
end end
@@ -1572,7 +1585,7 @@ class TestFish < TestBase
include TestShell include TestShell
def new_shell def new_shell
tmux.send_keys 'env FZF_TMUX=0 fish', :Enter tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? } tmux.until { |lines| lines.empty? }
end end