Compare commits

...

47 Commits

Author SHA1 Message Date
Junegunn Choi
355d004895 [neovim] Fix error with {'window': 'enew'} (#274) 2015-06-21 21:45:10 +09:00
Junegunn Choi
a336494f5d 0.10.0 2015-06-21 17:40:36 +09:00
Junegunn Choi
8270f7f0ca Rename --null to --read0 and undocument the option
`--null` is ambiguous. For completeness' sake, we need both `--read0`
and `--print0`.

`--read0` only makes sense when the input contains multiline entries.
However, fzf currently cannot correctly display multiline entries,
I'm going to make `--read0` an undocumented feature.
2015-06-21 17:29:58 +09:00
Junegunn Choi
638a956a9e Merge pull request #272 from okapia/zsh-simplify
Use vi-fetch-history on zsh to get history line
2015-06-21 16:57:14 +09:00
Oliver Kiddle
d395ebd28f use vi-fetch-history on zsh to get history line
In addition to being simpler, it allows subsequent up/down history
or accept-line-and-down-history widgets to work.
Also allow for find being and alias if alias expansion
after command is enabled.
2015-06-21 09:21:35 +02:00
Junegunn Choi
c0d3faa84f Hide --toggle-sort from --help output
Since the same can be now achieved with --bind KEY:toggle-sort
2015-06-19 01:06:56 +09:00
Junegunn Choi
3492c8b780 Rename --history-max to --history-size
Considering HISTSIZE and HISTFILESIZE of bash
2015-06-19 01:03:25 +09:00
Junegunn Choi
a8b2c257cd Improve handling of key names
Remember the exact string given as the key name so that it's possible to
correctly handle synonyms and print the original string.
2015-06-19 00:31:48 +09:00
Junegunn Choi
5e8d8dab82 More key names for --bind 2015-06-18 02:27:50 +09:00
Junegunn Choi
b504c6eb39 Avoid intermittent test failures
by making sure that we're back on shell command-line
2015-06-18 02:09:44 +09:00
Junegunn Choi
d261c36cde Keep the spinner spinning even when the source stream is idle 2015-06-18 00:42:38 +09:00
Junegunn Choi
fe4e452d68 Add --cycle option for cyclic scrolling
Close #266
2015-06-16 23:16:34 +09:00
Junegunn Choi
d54a4fa223 Add key name "bspace" for --bind (bspace != ctrl-h) 2015-06-16 02:18:49 +09:00
Junegunn Choi
45bd323cab Allow binding CTRL-G and CTRL-Q 2015-06-16 02:17:06 +09:00
Junegunn Choi
8677dbded1 Change alternative notation for execute action (#265)
e.g. fzf --bind "ctrl-m:execute:COMMAND..." --bind ctrl-j:accept
2015-06-15 23:27:05 +09:00
Junegunn Choi
794ad5785d Fix . to match newlines as well (#265) 2015-06-15 23:11:22 +09:00
Junegunn Choi
fa5b58968e Add alternative execute notation that does not require closing char
This can be used to avoid parse errors that can happen when the command
contains the closing character. Since the command does not finish at
a certain character, the key binding should be the last one in the
group. Suggested by @tiziano88. (#265)

  e.g. fzf --bind "ctrl-m:execute=COMMAND..." --bind ctrl-j:accept
2015-06-15 23:00:38 +09:00
Junegunn Choi
e720f56ea8 Fix test code for docker build 2015-06-15 22:45:31 +09:00
Junegunn Choi
7db53e6459 Add synonyms for some keys to be used with --bind and --toggle-sort
enter (return), space, tab, btab, esc, up, down, left, right
2015-06-15 01:26:18 +09:00
Junegunn Choi
e287bd7f04 Fix Travis CI build 2015-06-14 23:44:42 +09:00
Junegunn Choi
022435a90a More alternative notations for execute action
execute(...)
    execute[...]
    execute~...~
    execute!...!
    execute@...@
    execute#...#
    execute$...$
    execute%...%
    execute^...^
    execute&...&
    execute*...*
    execute:...:
    execute;...;
    execute/.../
    execute|...|
2015-06-14 23:36:49 +09:00
Junegunn Choi
6c99cc1700 Add bind action for executing arbitrary command (#265)
e.g. fzf --bind "ctrl-m:execute(less {})"
     fzf --bind "ctrl-t:execute[tmux new-window -d 'vim {}']"
2015-06-14 12:25:08 +09:00
Junegunn Choi
fe5b190a7d Remove unnecessary regexp matches
This change does have positive effect on startup time of fzf when many
number of options are provided.

    time fzf --query=____ --filter=____ --delimiter=q --prompt=________ \
    --nth=1,2,3,4 --with-nth=1,2,3,4 --toggle-sort=ctrl-r \
    --expect=ctrl-x --tiebreak=index --color=light --bind=ctrl-t:accept \
    --history=/tmp/xxx --history-max=1000 --help

    0m0.013s -> 0m0.008s
2015-06-14 11:23:07 +09:00
Junegunn Choi
77bab51696 GoLint fix 2015-06-14 03:19:18 +09:00
Junegunn Choi
77048f3e3b Fix Travis CI build 2015-06-14 02:51:45 +09:00
Junegunn Choi
8b618f7439 Test refactoring 2015-06-14 02:44:22 +09:00
Junegunn Choi
8973207bb4 Fix Travis CI build 2015-06-14 02:13:02 +09:00
Junegunn Choi
6ad1736832 Fix ignore action 2015-06-14 02:11:27 +09:00
Junegunn Choi
9fca611c4a Add ignore action for --bind 2015-06-14 01:54:56 +09:00
Junegunn Choi
8e7164553f Add missing files from the previous commit
:(
2015-06-14 00:53:45 +09:00
Junegunn Choi
3b52811796 Add support for search history
- Add `--history` option (e.g. fzf --history ~/.fzf.history)
- Add `--history-max` option for limiting the size of the file (default 1000)
- Add `previous-history` and `next-history` actions for `--bind`
    - CTRL-P and CTRL-N are automatically remapped to these actions when
      `--history` is used

Closes #249, #251
2015-06-14 00:48:48 +09:00
Junegunn Choi
2e84b1db64 Merge pull request #264 from kassio/master
Do not rename terminal buffer
2015-06-14 00:11:10 +09:00
Kassio Borges
9f33068ab3 Avoid conflict with other neoterm plugins.
To avoid conflict with other neoterm plugins that manage terminals,
prefer named terminals.
2015-06-13 11:13:33 -03:00
Junegunn Choi
eaa3c67a5e Add actions for --bind: select-all / deselect-all / toggle-all
Close #257
2015-06-09 23:44:54 +09:00
Junegunn Choi
1b9b1d15bc Adjust --help output 2015-06-08 23:28:41 +09:00
Junegunn Choi
97f433a274 Merge branch 'dullgiulio-121-accept-nil-input' 2015-06-08 23:28:06 +09:00
Junegunn Choi
45a3655eaf Add test case for --null option 2015-06-08 23:27:50 +09:00
Junegunn Choi
81ffde92fb Merge branch '121-accept-nil-input' of https://github.com/dullgiulio/fzf into dullgiulio-121-accept-nil-input 2015-06-08 23:21:16 +09:00
Junegunn Choi
0be4cead20 Allow ^EqualMatch$ 2015-06-08 23:17:24 +09:00
Giulio Iotti
f6dd32046e add support to nil-byte separated input strings, closes #121 2015-06-08 08:38:40 +00:00
Junegunn Choi
443a80f254 Always use the same color for multi-select markers 2015-06-07 23:32:07 +09:00
Junegunn Choi
8017635a71 Merge pull request #252 from dominikh/portable-swapOutput
Use ncurses's newterm instead of swapping stdout and stderr
2015-06-07 14:31:44 +09:00
Dominik Honnef
98f62b191a Use ncurses's newterm instead of swapping stdout and stderr 2015-06-07 07:26:26 +02:00
Junegunn Choi
52771a6226 0.9.13 2015-06-03 02:09:07 +09:00
Junegunn Choi
b00bcf506e Fix #248 - Premature termination of Reader on long input 2015-06-03 01:48:02 +09:00
Junegunn Choi
fdbfe36c0b Color customization (#245) 2015-06-03 01:46:03 +09:00
Junegunn Choi
446e822723 Update CHANGELOG 2015-05-22 02:37:38 +09:00
21 changed files with 1319 additions and 341 deletions

View File

@@ -1,6 +1,68 @@
CHANGELOG CHANGELOG
========= =========
0.10.0
------
### New features
- More actions for `--bind`
- `select-all`
- `deselect-all`
- `toggle-all`
- `ignore`
- `execute(...)` action for running arbitrary command without leaving fzf
- `fzf --bind "ctrl-m:execute(less {})"`
- `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"`
- If the command contains parentheses, use any of the follows alternative
notations to avoid parse errors
- `execute[...]`
- `execute~...~`
- `execute!...!`
- `execute@...@`
- `execute#...#`
- `execute$...$`
- `execute%...%`
- `execute^...^`
- `execute&...&`
- `execute*...*`
- `execute;...;`
- `execute/.../`
- `execute|...|`
- `execute:...`
- This is the special form that frees you from parse errors as it
does not expect the closing character
- The catch is that it should be the last one in the
comma-separated list
- Added support for optional search history
- `--history HISTORY_FILE`
- When used, `CTRL-N` and `CTRL-P` are automatically remapped to
`next-history` and `previous-history`
- `--history-size MAX_ENTRIES` (default: 1000)
- Cyclic scrolling can be enabled with `--cycle`
- Fixed the bug where the spinner was not spinning on idle input stream
- e.g. `sleep 100 | fzf`
### Minor improvements/fixes
- Added synonyms for key names that can be specified for `--bind`,
`--toggle-sort`, and `--expect`
- Fixed the color of multi-select marker on the current line
- Fixed to allow `^pattern$` in extended-search mode
0.9.13
------
### New features
- Color customization with the extended `--color` option
### Bug fixes
- Fixed premature termination of Reader in the presence of a long line which
is longer than 64KB
0.9.12 0.9.12
------ ------
@@ -11,6 +73,7 @@ CHANGELOG
### Bug fixes ### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize - Fixed to update "inline-info" immediately after terminal resize
- Fixed ANSI code offset calculation
0.9.11 0.9.11
------ ------

6
fzf
View File

@@ -206,11 +206,11 @@ class FZF
@expect = true @expect = true
when /^--expect=(.*)$/ when /^--expect=(.*)$/
@expect = true @expect = true
when '--toggle-sort', '--tiebreak', '--color', '--bind' when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size'
argv.shift argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
'--inline-info', '--no-inline-info', /^--bind=(.*)$/, '--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/,
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/
# XXX # XXX
else else
usage 1, "illegal option: #{o}" usage 1, "illegal option: #{o}"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.12 version=0.10.0
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "May 2015" "fzf 0.9.12" "fzf - a command-line fuzzy finder" .TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -52,10 +52,10 @@ Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details. See \fBFIELD INDEX EXPRESSION\fR for details.
.TP .TP
.BI "--with-nth=" "N[,..]" .BI "--with-nth=" "N[,..]"
Transform the item using the list of index expressions for search Transform each item using index expressions within finder
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.SS Search result .SS Search result
.TP .TP
.B "+s, --no-sort" .B "+s, --no-sort"
@@ -91,21 +91,38 @@ Enable processing of ANSI color codes
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
.B "--color=COL" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color scheme: [dark|light|16|bw] Color configuration. The name of the base color scheme is followed by custom
.br color mappings. Ansi color code of -1 denotes terminal default
(default: dark on 256-color terminal, otherwise 16) foreground/background color.
.br
.R "" .RS
.br e.g. \fBfzf --color=bg+:24\fR
.BR dark " Color scheme for dark 256-color terminal" \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.br .RE
.BR light " Color scheme for light 256-color terminal"
.br .RS
.BR 16 " Color scheme for 16-color terminal" .B BASE SCHEME:
.br (default: dark on 256-color terminal, otherwise 16)
.BR bw " No colors"
.br \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
.RE
.TP .TP
.B "--black" .B "--black"
Use black background Use black background
@@ -113,6 +130,9 @@ Use black background
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
.TP .TP
@@ -123,8 +143,7 @@ Display finder info inline with the query
Input prompt (default: '> ') Input prompt (default: '> ')
.TP .TP
.BI "--toggle-sort=" "KEY" .BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR.
or any single character)
.TP .TP
.BI "--bind=" "KEYBINDS" .BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. Each key binding expression Comma-separated list of custom key bindings. Each key binding expression
@@ -134,38 +153,109 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE .RE
.RS .RS
.B KEY: .B AVAILABLE KEYS:
\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character \fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\fR)
\fItab\fR
\fIbtab\fR (\fIshift-tab\fR)
\fIesc\fR
\fIdel\fR
\fIup\fR
\fIdown\fR
\fIleft\fR
\fIright\fR
\fIhome\fR
\fIend\fR
\fIpgup\fR (\fIpage-up\fR)
\fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR
\fIshift-right\fR
or any single character
.RE .RE
.RS .RS
.B ACTION: \fBACTION: DEFAULT BINDINGS:
abort \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
accept \fBaccept\fR \fIctrl-m (enter)\fR
backward-char \fBbackward-char\fR \fIctrl-b left\fR
backward-delete-char \fBbackward-delete-char\fR \fIctrl-h bspace\fR
backward-kill-word \fBbackward-kill-word\fR \fIalt-bs\fR
backward-word \fBbackward-word\fR \fIalt-b shift-left\fR
beginning-of-line \fBbeginning-of-line\fR \fIctrl-a home\fR
clear-screen \fBclear-screen\fR \fIctrl-l\fR
delete-char \fBdelete-char\fR \fIctrl-d del\fR
down \fBdeselect-all\fR
end-of-line \fBdown\fR \fIctrl-j ctrl-n down\fR
forward-char \fBend-of-line\fR \fIctrl-e end\fR
forward-word \fBexecute(...)\fR (see below for the details)
kill-line (not bound) \fBforward-char\fR \fIctrl-f right\fR
kill-word \fBforward-word\fR \fIalt-f shift-right\fR
page-down \fBignore\fR
page-up \fBkill-line\fR
toggle (not bound) \fBkill-word\fR \fIalt-d\fR
toggle-down \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
toggle-sort (not bound; equivalent to \fB--toggle-sort\fR) \fBpage-down\fR \fIpgdn\fR
toggle-up \fBpage-up\fR \fIpgup\fR
unix-line-discard \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
unix-word-rubout \fBselect-all\fR
up \fBtoggle\fR
yank \fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.RE .RE
.RS
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows.
.RS
\fBfzf --bind "enter:execute(less {})"\fR
.RE
\fB{}\fR is the placeholder for the double-quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
\fBexecute!...!\fR
\fBexecute@...@\fR
\fBexecute#...#\fR
\fBexecute$...$\fR
\fBexecute%...%\fR
\fBexecute^...^\fR
\fBexecute&...&\fR
\fBexecute*...*\fR
\fBexecute;...;\fR
\fBexecute/.../\fR
\fBexecute|...|\fR
\fBexecute:...\fR
.RS
This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list.
.RE
.RE
.TP
.BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion.
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
\fBnext-history\fR and \fBprevious-history\fR.
.TP
.BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value.
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -185,10 +275,9 @@ fzf becomes a fuzzy-version of grep.
Print query as the first line Print query as the first line
.TP .TP
.BI "--expect=" "KEY[,..]" .BI "--expect=" "KEY[,..]"
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, Comma-separated list of keys that can be used to complete fzf in addition to
or any single character) that can be used to complete fzf in addition to the the default enter key. When this option is set, fzf will print the name of the
default enter key. When this option is set, fzf will print the name of the key key pressed as the first line of its output (or as the second line if
pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed \fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key. with the default enter key.
.RS .RS
@@ -218,7 +307,7 @@ Default options. e.g. \fB--extended --ansi\fR
.SH FIELD INDEX EXPRESSION .SH FIELD INDEX EXPRESSION
A field index expression can be a non-zero integer or a range expression A field index expression can be a non-zero integer or a range expression
([BEGIN]..[END]). \fI--nth\fR and \fI--with-nth\fR take a comma-separated list ([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list
of field index expressions. of field index expressions.
.SS Examples .SS Examples
@@ -241,7 +330,7 @@ of field index expressions.
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search
mode". In this mode, you can specify multiple patterns delimited by spaces, mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
@@ -261,8 +350,8 @@ from the result.
.SS Extended-exact mode .SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing If you don't need fuzzy matching at all and do not wish to "quote" (prefixing
with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fI-x\fR or \fI--extended\fR). (instead of \fB-x\fR or \fB--extended\fR).
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -98,7 +98,7 @@ function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
set shell=sh set shell=sh
if has('nvim') && bufexists('[FZF]') if has('nvim') && bufexists('term://*:FZF')
echohl WarningMsg echohl WarningMsg
echomsg 'FZF is already running!' echomsg 'FZF is already running!'
echohl None echohl None
@@ -248,13 +248,17 @@ function! s:calc_size(max, val)
endif endif
endfunction endfunction
function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr()}
endfunction
function! s:split(dict) function! s:split(dict)
let directions = { let directions = {
\ 'up': ['topleft', 'resize', &lines], \ 'up': ['topleft', 'resize', &lines],
\ 'down': ['botright', 'resize', &lines], \ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ptab = tabpagenr() let s:ppos = s:getpos()
try try
for [dir, triple] in items(directions) for [dir, triple] in items(directions)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
@@ -280,32 +284,38 @@ function! s:execute_term(dict, command, temps)
call s:split(a:dict) call s:split(a:dict)
call s:pushd(a:dict) call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let tab = tabpagenr() let pos = s:getpos()
if bufnr('') == self.buf let inplace = pos == s:ppos " {'window': 'enew'}
" We use close instead of bd! since Vim does not close the split when if !inplace
" there's no other listed buffer if bufnr('') == self.buf
close " We use close instead of bd! since Vim does not close the split when
" FIXME This should be unnecessary due to `bufhidden=wipe` but in some " there's no other listed buffer (nvim +'set nobuflisted')
" cases Neovim fails to clean up the buffer and `bufexists('[FZF]') close
" returns 1 even when it cannot be seen anywhere else. e.g. `FZF!` endif
silent! execute 'bd!' self.buf if pos.tab == s:ppos.tab
endif wincmd p
if s:ptab == tab endif
wincmd p
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
try try
redraw! redraw!
call s:callback(self.dict, self.temps) call s:callback(self.dict, self.temps)
if inplace && bufnr('') == self.buf
execute "normal! \<c-^>"
" No other listed buffer
if bufnr('') == self.buf
bd!
endif
endif
finally finally
call s:popd(self.dict) call s:popd(self.dict)
endtry endtry
endfunction endfunction
call termopen(a:command, fzf) call termopen(a:command, fzf)
silent file [FZF]
startinsert startinsert
return [] return []
endfunction endfunction

View File

@@ -45,7 +45,10 @@ _fzf_opts_completion() {
--print-query --print-query
--expect --expect
--toggle-sort --toggle-sort
--sync" --sync
--cycle
--history
--history-size"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@@ -56,6 +59,10 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0 return 0
;; ;;
--history)
COMPREPLY=()
return 0
;;
esac esac
if [[ ${cur} =~ ^-|\+ ]]; then if [[ ${cur} =~ ^-|\+ ]]; then
@@ -207,7 +214,7 @@ EOF
} }
# fzf options # fzf options
complete -F _fzf_opts_completion fzf complete -o default -F _fzf_opts_completion fzf
d_cmds="cd pushd rmdir" d_cmds="cd pushd rmdir"
f_cmds=" f_cmds="

View File

@@ -26,7 +26,7 @@ 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() {
cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
zle reset-prompt zle reset-prompt
} }
@@ -36,16 +36,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 restore_no_bang_hist local selected restore_no_bang_hist
if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then
num=$(echo "$selected" | head -n1 | awk '{print $1}' | sed 's/[^0-9]//g') num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
LBUFFER=!$num zle vi-fetch-history -n $num
if setopt | grep nobanghist > /dev/null; then
restore_no_bang_hist=1
unsetopt no_bang_hist
fi
zle expand-history
[ -n "$restore_no_bang_hist" ] && setopt no_bang_hist
fi fi
fi fi
zle redisplay zle redisplay

View File

@@ -1,6 +1,7 @@
package algo package algo
import ( import (
"strings"
"unicode" "unicode"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -159,3 +160,17 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
} }
return trimmedLen - len(pattern), trimmedLen return trimmedLen - len(pattern), trimmedLen
} }
func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) != len(pattern) {
return -1, -1
}
runesStr := string(*runes)
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
if runesStr == string(pattern) {
return 0, len(pattern)
}
return -1, -1
}

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
Version = "0.9.12" Version = "0.10.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -32,6 +32,9 @@ const (
// Not to cache mergers with large lists // Not to cache mergers with large lists
mergerCacheMax int = 100000 mergerCacheMax int = 100000
// History
defaultHistoryMax int = 1000
) )
// fzf events // fzf events

View File

@@ -113,7 +113,7 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
} }
@@ -139,7 +139,7 @@ func Run(opts *Options) {
if pattern.MatchItem(item) { if pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(*item.text)
} }
}, eventBox} }, eventBox, opts.ReadZero}
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)

View File

@@ -4,11 +4,6 @@ package curses
#include <ncurses.h> #include <ncurses.h>
#include <locale.h> #include <locale.h>
#cgo LDFLAGS: -lncurses #cgo LDFLAGS: -lncurses
void swapOutput() {
FILE* temp = stdout;
stdout = stderr;
stderr = temp;
}
*/ */
import "C" import "C"
@@ -56,6 +51,7 @@ const (
Mouse Mouse
BTab BTab
BSpace
Del Del
PgUp PgUp
@@ -106,15 +102,18 @@ const (
) )
type ColorTheme struct { type ColorTheme struct {
darkBg C.short UseDefault bool
prompt C.short Fg int16
match C.short Bg int16
current C.short DarkBg int16
currentMatch C.short Prompt int16
spinner C.short Match int16
info C.short Current int16
cursor C.short CurrentMatch int16
selected C.short Spinner int16
Info int16
Cursor int16
Selected int16
} }
type Event struct { type Event struct {
@@ -139,10 +138,14 @@ var (
_colorMap map[int]int _colorMap map[int]int
_prevDownTime time.Time _prevDownTime time.Time
_clickY []int _clickY []int
_screen *C.SCREEN
Default16 *ColorTheme Default16 *ColorTheme
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
DarkBG C.short FG int
CurrentFG int
BG int
DarkBG int
) )
func init() { func init() {
@@ -150,35 +153,44 @@ func init() {
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int) _colorMap = make(map[int]int)
Default16 = &ColorTheme{ Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK, UseDefault: true,
prompt: C.COLOR_BLUE, Fg: 15,
match: C.COLOR_GREEN, Bg: 0,
current: C.COLOR_YELLOW, DarkBg: C.COLOR_BLACK,
currentMatch: C.COLOR_GREEN, Prompt: C.COLOR_BLUE,
spinner: C.COLOR_GREEN, Match: C.COLOR_GREEN,
info: C.COLOR_WHITE, Current: C.COLOR_YELLOW,
cursor: C.COLOR_RED, CurrentMatch: C.COLOR_GREEN,
selected: C.COLOR_MAGENTA} Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
darkBg: 236, UseDefault: true,
prompt: 110, Fg: 15,
match: 108, Bg: 0,
current: 254, DarkBg: 236,
currentMatch: 151, Prompt: 110,
spinner: 148, Match: 108,
info: 144, Current: 254,
cursor: 161, CurrentMatch: 151,
selected: 168} Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
darkBg: 251, UseDefault: true,
prompt: 25, Fg: 15,
match: 66, Bg: 0,
current: 237, DarkBg: 251,
currentMatch: 23, Prompt: 25,
spinner: 65, Match: 66,
info: 101, Current: 237,
cursor: 161, CurrentMatch: 23,
selected: 168} Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -239,10 +251,9 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
} }
C.swapOutput()
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
C.initscr() _screen = C.newterm(nil, C.stderr, C.stdin)
C.set_term(_screen)
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
} }
@@ -268,28 +279,40 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
func initPairs(theme *ColorTheme, black bool) { func initPairs(theme *ColorTheme, black bool) {
var bg C.short fg := C.short(theme.Fg)
bg := C.short(theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else { } else if theme.UseDefault {
C.use_default_colors() fg = -1
bg = -1 bg = -1
C.use_default_colors()
}
if theme.UseDefault {
FG = -1
BG = -1
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
} }
DarkBG = theme.darkBg CurrentFG = int(theme.Current)
C.init_pair(ColPrompt, theme.prompt, bg) DarkBG = int(theme.DarkBg)
C.init_pair(ColMatch, theme.match, bg) darkBG := C.short(DarkBG)
C.init_pair(ColCurrent, theme.current, DarkBG) C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) C.init_pair(ColMatch, C.short(theme.Match), bg)
C.init_pair(ColSpinner, theme.spinner, bg) C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
C.init_pair(ColInfo, theme.info, bg) C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
C.init_pair(ColCursor, theme.cursor, DarkBG) C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
C.init_pair(ColSelected, theme.selected, DarkBG) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
} }
func Close() { func Close() {
C.endwin() C.endwin()
C.swapOutput() C.delscreen(_screen)
} }
func GetBytes() []byte { func GetBytes() []byte {
@@ -454,10 +477,14 @@ func GetChar() Event {
}() }()
switch _buf[0] { switch _buf[0] {
case CtrlC, CtrlG, CtrlQ: case CtrlC:
return Event{CtrlC, 0, nil} return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{CtrlH, 0, nil} return Event{BSpace, 0, nil}
case ESC: case ESC:
return escSequence(&sz) return escSequence(&sz)
} }

94
src/history.go Normal file
View File

@@ -0,0 +1,94 @@
package fzf
import (
"errors"
"io/ioutil"
"os"
"strings"
)
type History struct {
path string
lines []string
modified map[int]string
maxSize int
cursor int
}
func NewHistory(path string, maxSize int) (*History, error) {
fmtError := func(e error) error {
if os.IsPermission(e) {
return errors.New("permission denied: " + path)
}
return errors.New("invalid history file: " + e.Error())
}
// Read history file
data, err := ioutil.ReadFile(path)
if err != nil {
// If it doesn't exist, check if we can create a file with the name
if os.IsNotExist(err) {
data = []byte{}
if err := ioutil.WriteFile(path, data, 0600); err != nil {
return nil, fmtError(err)
}
} else {
return nil, fmtError(err)
}
}
// Split lines and limit the maximum number of lines
lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
if len(lines[len(lines)-1]) > 0 {
lines = append(lines, "")
}
return &History{
path: path,
maxSize: maxSize,
lines: lines,
modified: make(map[int]string),
cursor: len(lines) - 1}, nil
}
func (h *History) append(line string) error {
// We don't append empty lines
if len(line) == 0 {
return nil
}
lines := append(h.lines[:len(h.lines)-1], line)
if len(lines) > h.maxSize {
lines = lines[len(lines)-h.maxSize : len(lines)]
}
h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
}
func (h *History) override(str string) {
// You can update the history but they're not written to the file
if h.cursor == len(h.lines)-1 {
h.lines[h.cursor] = str
} else if h.cursor < len(h.lines)-1 {
h.modified[h.cursor] = str
}
}
func (h *History) current() string {
if str, prs := h.modified[h.cursor]; prs {
return str
}
return h.lines[h.cursor]
}
func (h *History) previous() string {
if h.cursor > 0 {
h.cursor--
}
return h.current()
}
func (h *History) next() string {
if h.cursor < len(h.lines)-1 {
h.cursor++
}
return h.current()
}

59
src/history_test.go Normal file
View File

@@ -0,0 +1,59 @@
package fzf
import (
"os/user"
"testing"
)
func TestHistory(t *testing.T) {
maxHistory := 50
// Invalid arguments
user, _ := user.Current()
paths := []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path)
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
for i := 0; i < maxHistory+10; i++ {
h.append("foobar")
}
}
{ // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
for i := 0; i < maxHistory; i++ {
if h.lines[i] != "foobar" {
t.Error("Expected: foobar, actual: " + h.lines[i])
}
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h.append("barfoo")
h.append("")
h.append("foobarbaz")
}
{ // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
compare := func(idx int, exp string) {
if h.lines[idx] != exp {
t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx])
}
}
compare(maxHistory-3, "foobar")
compare(maxHistory-2, "barfoo")
compare(maxHistory-1, "foobarbaz")
}
}

View File

@@ -86,10 +86,15 @@ func (i *Item) Rank(cache bool) Rank {
// AsString returns the original string // AsString returns the original string
func (i *Item) AsString() string { func (i *Item) AsString() string {
return *i.StringPtr()
}
// StringPtr returns the pointer to the original string
func (i *Item) StringPtr() *string {
if i.origText != nil { if i.origText != nil {
return *i.origText return i.origText
} }
return *i.text return i.text
} }
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
@@ -143,13 +148,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else { } else {
ansi := item.colors[curr-1] 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 bg := ansi.color.bg
if current && bg == -1 { if bg == -1 {
bg = int(curses.DarkBG) if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
} }
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) bold: ansi.color.bold || bold})
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -14,7 +15,7 @@ import (
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
Search mode Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
@@ -22,10 +23,8 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform the item using index expressions for search --with-nth=N[,..] Transform item using index expressions within finder
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied; --tiebreak=CRI Sort criterion when the scores are tied;
@@ -35,15 +34,16 @@ const usage = `usage: fzf [options]
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COL Color scheme; [dark|light|16|bw] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
(default: dark on 256-color terminal, otherwise 16)
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--toggle-sort=KEY Key to toggle sort
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -106,6 +106,7 @@ type Options struct {
Theme *curses.ColorTheme Theme *curses.ColorTheme
Black bool Black bool
Reverse bool Reverse bool
Cycle bool
Hscroll bool Hscroll bool
InlineInfo bool InlineInfo bool
Prompt string Prompt string
@@ -114,21 +115,24 @@ type Options struct {
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect []int Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string
PrintQuery bool PrintQuery bool
ReadZero bool
Sync bool Sync bool
History *History
Version bool Version bool
} }
func defaultOptions() *Options { func defaultTheme() *curses.ColorTheme {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") { if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256 return curses.Dark256
} else {
defaultTheme = curses.Default16
} }
return curses.Default16
}
func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
@@ -141,9 +145,10 @@ func defaultOptions() *Options {
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme, Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false,
Hscroll: true, Hscroll: true,
InlineInfo: false, InlineInfo: false,
Prompt: "> ", Prompt: "> ",
@@ -152,10 +157,13 @@ func defaultOptions() *Options {
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: []int{}, Expect: make(map[int]string),
Keymap: defaultKeymap(), Keymap: defaultKeymap(),
Execmap: make(map[int]string),
PrintQuery: false, PrintQuery: false,
ReadZero: false,
Sync: false, Sync: false,
History: nil,
Version: false} Version: false}
} }
@@ -169,11 +177,11 @@ func errorExit(msg string) {
help(1) help(1)
} }
func optString(arg string, prefix string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix)) for _, prefix := range prefixes {
matches := rx.FindStringSubmatch(arg) if strings.HasPrefix(arg, prefix) {
if len(matches) > 1 { return true, arg[len(prefix):]
return true, matches[1] }
} }
return false, "" return false, ""
} }
@@ -187,6 +195,31 @@ func nextString(args []string, i *int, message string) string {
return args[*i] return args[*i]
} }
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
*i++
return args[*i]
}
return ""
}
func atoi(str string) int {
num, err := strconv.Atoi(str)
if err != nil {
errorExit("not a valid integer: " + str)
}
return num
}
func nextInt(args []string, i *int, message string) int {
if len(args) > *i+1 {
*i++
} else {
errorExit(message)
}
return atoi(args[*i])
}
func optionalNumeric(args []string, i *int) int { func optionalNumeric(args []string, i *int) int {
if len(args) > *i+1 { if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 { if strings.IndexAny(args[*i+1], "0123456789") == 0 {
@@ -230,7 +263,7 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func parseKeyChords(str string, message string) []int { func parseKeyChords(str string, message string) map[int]string {
if len(str) == 0 { if len(str) == 0 {
errorExit(message) errorExit(message)
} }
@@ -240,22 +273,65 @@ func parseKeyChords(str string, message string) []int {
tokens = append(tokens, ",") tokens = append(tokens, ",")
} }
var chords []int chords := make(map[int]string)
for _, key := range tokens { for _, key := range tokens {
if len(key) == 0 { if len(key) == 0 {
continue // ignore continue // ignore
} }
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord := 0
chords = append(chords, curses.CtrlA+int(lkey[5])-'a') switch lkey {
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { case "up":
chords = append(chords, curses.AltA+int(lkey[4])-'a') chord = curses.Up
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { case "down":
chords = append(chords, curses.F1+int(key[1])-'1') chord = curses.Down
} else if utf8.RuneCountInString(key) == 1 { case "left":
chords = append(chords, curses.AltZ+int([]rune(key)[0])) chord = curses.Left
} else { case "right":
errorExit("unsupported key: " + key) chord = curses.Right
case "enter", "return":
chord = curses.CtrlM
case "space":
chord = curses.AltZ + int(' ')
case "bspace", "bs":
chord = curses.BSpace
case "alt-bs", "alt-bspace":
chord = curses.AltBS
case "tab":
chord = curses.Tab
case "btab", "shift-tab":
chord = curses.BTab
case "esc":
chord = curses.ESC
case "del":
chord = curses.Del
case "home":
chord = curses.Home
case "end":
chord = curses.End
case "pgup", "page-up":
chord = curses.PgUp
case "pgdn", "page-down":
chord = curses.PgDn
case "shift-left":
chord = curses.SLeft
case "shift-right":
chord = curses.SRight
default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chord = curses.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0])
} else {
errorExit("unsupported key: " + key)
}
}
if chord > 0 {
chords[chord] = key
} }
} }
return chords return chords
@@ -277,28 +353,103 @@ func parseTiebreak(str string) tiebreak {
return byLength return byLength
} }
func parseTheme(str string) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
switch strings.ToLower(str) { dupe := *theme
case "dark": return &dupe
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
} }
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
for _, pairStr := range strings.Split(str, ",") { theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(curses.Dark256)
case "light":
theme = dupeTheme(curses.Light256)
case "16":
theme = dupeTheme(curses.Default16)
case "bw", "no":
theme = nil
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
errorExit("colors disabled; cannot customize colors")
}
pair := strings.Split(str, ":")
if len(pair) != 2 {
fail()
}
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "hl":
theme.Match = ansi
case "hl+":
theme.CurrentMatch = ansi
case "prompt":
theme.Prompt = ansi
case "spinner":
theme.Spinner = ansi
case "info":
theme.Info = ansi
case "pointer":
theme.Cursor = ansi
case "marker":
theme.Selected = ansi
default:
fail()
}
}
}
return theme
}
var executeRegexp *regexp.Regexp
func firstKey(keymap map[int]string) int {
for k := range keymap {
return k
}
return 0
}
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
if executeRegexp == nil {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
}
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
})
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
pairStr = str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
fail := func() { fail := func() {
errorExit("invalid key binding: " + pairStr) errorExit("invalid key binding: " + pairStr)
} }
pair := strings.Split(pairStr, ":") pair := strings.SplitN(pairStr, ":", 2)
if len(pair) != 2 { if len(pair) != 2 {
fail() fail()
} }
@@ -306,9 +457,11 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
if len(keys) != 1 { if len(keys) != 1 {
fail() fail()
} }
key := keys[0] key := firstKey(keys)
act := strings.ToLower(pair[1]) act := strings.ToLower(pair[1])
switch act { switch act {
case "ignore":
keymap[key] = actIgnore
case "beginning-of-line": case "beginning-of-line":
keymap[key] = actBeginningOfLine keymap[key] = actBeginningOfLine
case "abort": case "abort":
@@ -347,6 +500,12 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
keymap[key] = actToggleDown keymap[key] = actToggleDown
case "toggle-up": case "toggle-up":
keymap[key] = actToggleUp keymap[key] = actToggleUp
case "toggle-all":
keymap[key] = actToggleAll
case "select-all":
keymap[key] = actSelectAll
case "deselect-all":
keymap[key] = actDeselectAll
case "toggle": case "toggle":
keymap[key] = actToggle keymap[key] = actToggle
case "down": case "down":
@@ -357,14 +516,40 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
keymap[key] = actPageUp keymap[key] = actPageUp
case "page-down": case "page-down":
keymap[key] = actPageDown keymap[key] = actPageDown
case "previous-history":
keymap[key] = actPreviousHistory
case "next-history":
keymap[key] = actNextHistory
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
toggleSort = true toggleSort = true
default: default:
errorExit("unknown action: " + act) if isExecuteAction(act) {
keymap[key] = actExecute
if pair[1][7] == ':' {
execmap[key] = pair[1][8:]
} else {
execmap[key] = pair[1][8 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
}
} }
} }
return keymap, toggleSort return keymap, execmap, toggleSort
}
func isExecuteAction(str string) bool {
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
return false
}
b := str[7]
e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
return true
}
return false
} }
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
@@ -372,11 +557,34 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
keymap[keys[0]] = actToggleSort keymap[firstKey(keys)] = actToggleSort
return keymap return keymap
} }
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType)
var historyMax int
if opts.History == nil {
historyMax = defaultHistoryMax
} else {
historyMax = opts.History.maxSize
}
setHistory := func(path string) {
h, e := NewHistory(path, historyMax)
if e != nil {
errorExit(e.Error())
}
opts.History = h
}
setHistoryMax := func(max int) {
historyMax = max
if historyMax < 1 {
errorExit("history max must be a positive integer")
}
if opts.History != nil {
opts.History.maxSize = historyMax
}
}
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -398,11 +606,17 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak": case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = defaultTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
case "--toggle-sort": case "--toggle-sort":
opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true opts.ToggleSort = true
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
@@ -444,6 +658,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true opts.Reverse = true
case "--no-reverse": case "--no-reverse":
opts.Reverse = false opts.Reverse = false
case "--cycle":
opts.Cycle = true
case "--no-cycle":
opts.Cycle = false
case "--hscroll": case "--hscroll":
opts.Hscroll = true opts.Hscroll = true
case "--no-hscroll": case "--no-hscroll":
@@ -460,6 +678,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Exit0 = true opts.Exit0 = true
case "+0", "--no-exit-0": case "+0", "--no-exit-0":
opts.Exit0 = false opts.Exit0 = false
case "--read0":
opts.ReadZero = true
case "--no-read0":
opts.ReadZero = false
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":
@@ -472,40 +694,66 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sync = false opts.Sync = false
case "--async": case "--async":
opts.Sync = false opts.Sync = false
case "--no-history":
opts.History = nil
case "--history":
setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
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, "-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
} else if match, value := optString(arg, "-d|--delimiter="); match { } else if match, value := optString(arg, "-d", "--delimiter="); match {
opts.Delimiter = delimiterRegexp(value) opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "-n|--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value) opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match { } else if match, value := optString(arg, "--with-nth="); match {
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, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
opts.Keymap = checkToggleSort(opts.Keymap, value) keymap = checkToggleSort(keymap, value)
opts.ToggleSort = true opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value) opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
} else if match, value := optString(arg, "--history="); match {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value))
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
} }
} }
// Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs {
keymap[curses.CtrlP] = actPreviousHistory
}
if _, prs := keymap[curses.CtrlN]; !prs {
keymap[curses.CtrlN] = actNextHistory
}
}
// Override default key bindings
for key, act := range keymap {
opts.Keymap[key] = act
}
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 { if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
@@ -71,63 +72,101 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) { check := func(i int, s string) {
if key != expected { if pairs[i] != s {
t.Errorf("%d != %d", key, expected) t.Errorf("%s != %s", pairs[i], s)
} }
} }
check(len(keys), 9) if len(pairs) != 9 {
check(keys[0], curses.CtrlZ) t.Error(9)
check(keys[1], curses.AltZ) }
check(keys[2], curses.F2) check(curses.CtrlZ, "ctrl-z")
check(keys[3], curses.AltZ+'@') check(curses.AltZ, "alt-z")
check(keys[4], curses.AltA) check(curses.F2, "f2")
check(keys[5], curses.AltZ+'!') check(curses.AltZ+'@', "@")
check(keys[6], curses.CtrlA+'g'-'a') check(curses.AltA, "Alt-a")
check(keys[7], curses.AltZ+'J') check(curses.AltZ+'!', "!")
check(keys[8], curses.AltZ+'g') check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g")
// Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
check(curses.CtrlM, "Return")
check(curses.AltZ+' ', "space")
check(curses.Tab, "tab")
check(curses.BTab, "btab")
check(curses.ESC, "esc")
check(curses.Up, "up")
check(curses.Down, "down")
check(curses.Left, "left")
check(curses.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", "")
if len(pairs) != 11 {
t.Error(11)
}
check(curses.Tab, "Ctrl-I")
check(curses.PgUp, "page-up")
check(curses.PgDn, "Page-Down")
check(curses.Home, "Home")
check(curses.End, "End")
check(curses.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left")
check(curses.SRight, "shift-right")
check(curses.BTab, "shift-tab")
check(curses.CtrlM, "Enter")
check(curses.BSpace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) { checkN := func(a int, b int) {
if key != expected { if a != b {
t.Errorf("%d != %d", key, expected) t.Errorf("%d != %d", a, b)
}
}
check := func(pairs map[int]string, i int, s string) {
if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s)
} }
} }
keys := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
check(len(keys), 1) checkN(len(pairs), 1)
check(keys[0], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
check(len(keys), 4) checkN(len(pairs), 4)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+'c') check(pairs, curses.AltZ+'c', "c")
check(keys[3], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
check(len(keys), 1) checkN(len(pairs), 1)
check(keys[0], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
@@ -136,11 +175,20 @@ func TestBind(t *testing.T) {
t.Errorf("%d != %d", action, expected) t.Errorf("%d != %d", action, expected)
} }
} }
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA]) check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, toggleSort := keymap, execmap, toggleSort :=
parseKeymap(keymap, false, parseKeymap(keymap, execmap, false,
"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 {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort { if !toggleSort {
t.Errorf("toggleSort not set") t.Errorf("toggleSort not set")
} }
@@ -148,10 +196,73 @@ func TestBind(t *testing.T) {
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[curses.AltZ+'c'])
check(actPageDown, keymap[curses.AltZ]) check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2])
check(actExecute, keymap[curses.F3])
check(actExecute, keymap[curses.F4])
checkString("ls {}", execmap[curses.F1])
checkString("echo {}, {}, {}", execmap[curses.F2])
checkString("echo '({})'", execmap[curses.F3])
checkString("less {}", execmap[curses.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
keymap, toggleSort = parseKeymap(keymap, false, "f1:abort") for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
keymap, execmap, toggleSort =
parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
}
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort")
if toggleSort { if toggleSort {
t.Errorf("toggleSort set") t.Errorf("toggleSort set")
} }
check(actAbort, keymap[curses.F1]) check(actAbort, keymap[curses.F1])
} }
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
if dark == theme {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}

View File

@@ -24,6 +24,7 @@ const (
termExact termExact
termPrefix termPrefix
termSuffix termSuffix
termEqual
) )
type term struct { type term struct {
@@ -116,6 +117,7 @@ func BuildPattern(mode Mode, caseMode Case,
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
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
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
@@ -151,8 +153,13 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
text = text[1:] text = text[1:]
} }
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
typ = termPrefix if strings.HasSuffix(text, "$") {
text = text[1:] typ = termEqual
text = text[1 : len(text)-1]
} else {
typ = termPrefix
text = text[1:]
}
} else if strings.HasSuffix(text, "$") { } else if strings.HasSuffix(text, "$") {
typ = termSuffix typ = termSuffix
text = text[:len(text)-1] text = text[:len(text)-1]

View File

@@ -8,8 +8,8 @@ import (
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, CaseSmart, terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
if len(terms) != 8 || if len(terms) != 9 ||
terms[0].typ != termFuzzy || terms[0].inv || terms[0].typ != termFuzzy || terms[0].inv ||
terms[1].typ != termExact || terms[1].inv || terms[1].typ != termExact || terms[1].inv ||
terms[2].typ != termPrefix || terms[2].inv || terms[2].typ != termPrefix || terms[2].inv ||
@@ -17,7 +17,8 @@ func TestParseTermsExtended(t *testing.T) {
terms[4].typ != termFuzzy || !terms[4].inv || terms[4].typ != termFuzzy || !terms[4].inv ||
terms[5].typ != termExact || !terms[5].inv || terms[5].typ != termExact || !terms[5].inv ||
terms[6].typ != termPrefix || !terms[6].inv || terms[6].typ != termPrefix || !terms[6].inv ||
terms[7].typ != termSuffix || !terms[7].inv { terms[7].typ != termSuffix || !terms[7].inv ||
terms[8].typ != termEqual || terms[8].inv {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
for idx, term := range terms { for idx, term := range terms {
@@ -65,6 +66,22 @@ func TestExact(t *testing.T) {
} }
} }
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
runes := []rune(str)
sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
}
match("ABC", -1, -1)
match("AbC", 0, 3)
}
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()

View File

@@ -13,6 +13,7 @@ import (
type Reader struct { type Reader struct {
pusher func(string) pusher func(string)
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
@@ -30,11 +31,24 @@ func (r *Reader) ReadSource() {
} }
func (r *Reader) feed(src io.Reader) { func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil { delim := byte('\n')
for scanner.Scan() { if r.delimNil {
r.pusher(scanner.Text()) delim = '\000'
}
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString(delim)
if line != "" {
// "ReadString returns err != nil if and only if the returned data does not end in delim."
if err == nil {
line = line[:len(line)-1]
}
r.pusher(line)
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
} }
if err != nil {
break
}
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
@@ -32,10 +33,13 @@ type Terminal struct {
multi bool multi bool
sort bool sort bool
toggleSort bool toggleSort bool
expect []int expect map[int]string
keymap map[int]actionType keymap map[int]actionType
pressed int execmap map[int]string
pressed string
printQuery bool printQuery bool
history *History
cycle bool
count int count int
progress int progress int
reading bool reading bool
@@ -105,7 +109,10 @@ const (
actUnixWordRubout actUnixWordRubout
actYank actYank
actBackwardKillWord actBackwardKillWord
actSelectAll
actDeselectAll
actToggle actToggle
actToggleAll
actToggleDown actToggleDown
actToggleUp actToggleUp
actDown actDown
@@ -113,6 +120,9 @@ const (
actPageUp actPageUp
actPageDown actPageDown
actToggleSort actToggleSort
actPreviousHistory
actNextHistory
actExecute
) )
func defaultKeymap() map[int]actionType { func defaultKeymap() map[int]actionType {
@@ -128,6 +138,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown keymap[C.CtrlJ] = actDown
@@ -181,8 +192,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
pressed: 0, execmap: opts.Execmap,
pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History,
cycle: opts.Cycle,
reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
@@ -242,17 +257,7 @@ func (t *Terminal) output() {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
} }
if len(t.expect) > 0 { if len(t.expect) > 0 {
if t.pressed == 0 { fmt.Println(t.pressed)
fmt.Println()
} else if util.Between(t.pressed, C.AltA, C.AltZ) {
fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA)
} else if util.Between(t.pressed, C.F1, C.F4) {
fmt.Printf("f%c\n", t.pressed+'1'-C.F1)
} else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) {
fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA)
} else {
fmt.Printf("%c\n", t.pressed-C.AltZ)
}
} }
if len(t.selected) == 0 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
@@ -373,7 +378,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
if current { if current {
C.CPrint(C.ColCursor, true, ">") C.CPrint(C.ColCursor, true, ">")
if selected { if selected {
C.CPrint(C.ColCurrent, true, ">") C.CPrint(C.ColSelected, true, ">")
} else { } else {
C.CPrint(C.ColCurrent, true, " ") C.CPrint(C.ColCurrent, true, " ")
} }
@@ -580,6 +585,17 @@ func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
} }
func executeCommand(template string, current string) {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
@@ -605,6 +621,27 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqRedraw, nil) t.reqBox.Set(reqRedraw, nil)
} }
}() }()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
}
exit := func(code int) {
if code == 0 && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
} }
go func() { go func() {
@@ -633,10 +670,10 @@ func (t *Terminal) Loop() {
case reqClose: case reqClose:
C.Close() C.Close()
t.output() t.output()
os.Exit(0) exit(0)
case reqQuit: case reqQuit:
C.Close() C.Close()
os.Exit(1) exit(1)
} }
} }
t.placeCursor() t.placeCursor()
@@ -661,39 +698,48 @@ func (t *Terminal) Loop() {
} }
} }
} }
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
if !selectItem(item) {
delete(t.selected, item.index)
}
}
toggle := func() { toggle := func() {
if t.cy < t.merger.Length() { if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) toggleY(t.cy)
if _, found := t.selected[item.index]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.index] = selectedItem{time.Now(), strptr}
} else {
delete(t.selected, item.index)
}
req(reqInfo) req(reqInfo)
} }
} }
for _, key := range t.expect { for key, ret := range t.expect {
if keyMatch(key, event) { if keyMatch(key, event) {
t.pressed = key t.pressed = ret
req(reqClose) req(reqClose)
break break
} }
} }
action := t.keymap[event.Type] action := t.keymap[event.Type]
mapkey := event.Type
if event.Type == C.Rune { if event.Type == C.Rune {
code := int(event.Char) + int(C.AltZ) mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[code]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act action = act
} }
} }
switch action { switch action {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString())
}
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
continue continue
@@ -725,11 +771,34 @@ func (t *Terminal) Loop() {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx-- t.cx--
} }
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle: case actToggle:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
req(reqList) req(reqList)
} }
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown: case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
@@ -796,6 +865,18 @@ func (t *Terminal) Loop() {
prefix := copySlice(t.input[:t.cx]) prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++ t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
@@ -871,10 +952,22 @@ func (t *Terminal) constrain() {
func (t *Terminal) vmove(o int) { func (t *Terminal) vmove(o int) {
if t.reverse { if t.reverse {
t.vset(t.cy - o) o *= -1
} else {
t.vset(t.cy + o)
} }
dest := t.cy + o
if t.cycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
}
t.vset(dest)
} }
func (t *Terminal) vset(o int) bool { func (t *Terminal) vset(o int) bool {
@@ -885,7 +978,6 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
if t.inlineInfo { if t.inlineInfo {
return C.MaxY() - 1 return C.MaxY() - 1
} else {
return C.MaxY() - 2
} }
return C.MaxY() - 2
} }

View File

@@ -72,17 +72,6 @@ class Tmux
end end
end end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close
wait do
send_keys 'C-c', 'C-u', 'exit', :Enter
closed?
end
end
def kill def kill
go("kill-window -t #{win} 2> /dev/null") go("kill-window -t #{win} 2> /dev/null")
end end
@@ -152,21 +141,26 @@ class TestBase < Minitest::Test
attr_reader :tmux attr_reader :tmux
def tempname
[TEMPNAME,
caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-'
end
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end end
def readonce def readonce
wait { File.exists?(TEMPNAME) } wait { File.exists?(tempname) }
File.read(TEMPNAME) File.read(tempname)
ensure ensure
File.unlink TEMPNAME while File.exists?(TEMPNAME) File.unlink tempname while File.exists?(tempname)
tmux.prepare
end end
def fzf(*opts) def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}" fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}"
end end
def fzf!(*opts) def fzf!(*opts)
@@ -214,7 +208,6 @@ class TestGoFZF < TestBase
assert_equal '> 391', lines[-1] assert_equal '> 391', lines[-1]
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close
assert_equal '1391', readonce.chomp assert_equal '1391', readonce.chomp
end end
@@ -223,7 +216,6 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readonce.chomp assert_equal 'hello', readonce.chomp
end end
@@ -290,7 +282,6 @@ class TestGoFZF < TestBase
# CTRL-M # CTRL-M
tmux.send_keys "C-M" tmux.send_keys "C-M"
tmux.until { |lines| lines.last !~ /^>/ } tmux.until { |lines| lines.last !~ /^>/ }
tmux.close
end end
def test_multi_order def test_multi_order
@@ -303,7 +294,6 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '(6)' } tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M" tmux.send_keys "C-M"
assert_equal %w[3 2 5 6 8 7], readonce.split($/) assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end end
def test_with_nth def test_with_nth
@@ -454,16 +444,12 @@ class TestGoFZF < TestBase
end end
def test_unicode_case def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end end
def test_tiebreak def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[ input = %w[
--foobar-------- --foobar--------
-----foobar--- -----foobar---
@@ -500,8 +486,6 @@ class TestGoFZF < TestBase
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end end
def test_invalid_cache def test_invalid_cache
@@ -525,6 +509,132 @@ class TestGoFZF < TestBase
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.split($/) assert_equal %w[4 5 6 9], readonce.split($/)
end end
def test_long_line
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
end
def test_read0
lines = `find .`.split($/)
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
end
def test_select_all_deselect_all_toggle_all
tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include? '(3)' }
tmux.send_keys 'C-t'
tmux.until { |lines| lines[-2].include? '(97)' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(100)' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(98)' }
tmux.send_keys 'C-d'
tmux.until { |lines| !lines[-2].include? '(' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(2)' }
tmux.send_keys 0
tmux.until { |lines| lines[-2].include? '10/100' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(12)' }
tmux.send_keys :Enter
assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/)
end
def test_history
history_file = '/tmp/fzf-test-history'
# History with limited number of entries
File.unlink history_file rescue nil
opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44].map { |e| e + $/ }
input.each do |keys|
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys keys
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Enter
readonce
end
assert_equal input[1..-1], File.readlines(history_file)
# Update history entries (not changed on disk)
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 44' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 33' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-1].end_with? '> 3' }
tmux.send_keys 1
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 22' }
tmux.send_keys 'C-n'
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 0
tmux.until { |lines| lines[-1].end_with? '> 310' }
tmux.send_keys :Enter
readonce
assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file)
# Respect --bind option
tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
tmux.until { |lines| lines[-1].end_with?('33') }
tmux.send_keys :Enter
ensure
File.unlink history_file
end
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a, :Escape, :a
tmux.send_keys :Up
tmux.send_keys :Escape, :b, :Escape, :b
tmux.send_keys :Up
tmux.send_keys :C
tmux.send_keys 'foobar'
tmux.until { |lines| lines[-2].include? '0/100' }
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
tmux.send_keys :Enter
readonce
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
File.readlines(output).map(&:chomp)
ensure
File.unlink output rescue nil
end
def test_cycle
tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
tmux.until { |lines| lines[-2].include? '8/8' }
tmux.send_keys :Down
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :Down
tmux.until { |lines| lines[-9].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :Up
tmux.until { |lines| lines[-4].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-10].start_with? '>' }
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -563,7 +673,7 @@ module TestShell
def test_alt_c def test_alt_c
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c, pane: 0 tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 0 } lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
expected = lines[-3][2..-1] expected = lines[-3][2..-1]
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.prepare tmux.prepare
@@ -697,6 +807,7 @@ class TestBash < TestBase
include CompletionTest include CompletionTest
def new_shell def new_shell
tmux.prepare
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare tmux.prepare
end end