Compare commits

..

29 Commits

Author SHA1 Message Date
Junegunn Choi
248320fa55 0.11.1 2015-12-01 00:39:45 +09:00
Junegunn Choi
d4e26707c7 GO15VENDOREXPERIMENT=1 (#430) 2015-11-30 18:41:53 +09:00
Junegunn Choi
99ea1056ac Add --tabstop option
Related: https://github.com/junegunn/fzf.vim/issues/49
2015-11-30 17:35:03 +09:00
Junegunn Choi
7bcf4effa5 Fix test failure - use absolute path 2015-11-30 17:32:40 +09:00
Junegunn Choi
e1df876b61 Merge pull request #380 from acornejo/android
Add android build: `make android`
2015-11-20 03:28:41 +09:00
Alex Cornejo
28ffb9638d add android build 2015-11-18 23:20:51 -08:00
Junegunn Choi
1c20255504 Fix typos in help message
Close #425. Thanks to @blueyed.
2015-11-19 09:58:07 +09:00
Junegunn Choi
1fd884b34f Merge pull request #423 from blueyed/zsh-fzf-completion-localoptions
zsh: fzf-completion: use noshwordsplit local option
2015-11-19 00:54:42 +09:00
Daniel Hahler
701687faab zsh: fzf-completion: use noshwordsplit local option
This also fixes the completion causing a bell / flickering in case
"shwordsplit" was not set, because then the function would return false.
2015-11-18 16:09:00 +01:00
Junegunn Choi
bbc3055feb Merge pull request #421 from blueyed/zsh-completion-grep-command
zsh completion: use \grep to skip any alias
2015-11-18 03:32:39 +09:00
Daniel Hahler
95c69083c7 zsh completion: use \grep to skip any alias 2015-11-17 18:51:29 +01:00
Junegunn Choi
57a37b5832 [bash-completion] Fix #417 - Update command list 2015-11-12 13:53:36 +09:00
Junegunn Choi
d29ae1c462 [install] Add --32 / --64 options
Related: #373
2015-11-12 13:42:56 +09:00
Junegunn Choi
df468fc482 0.11.0 2015-11-10 01:54:53 +09:00
Junegunn Choi
31278bcc68 Fix compatibility issues with OR operator and inverse terms 2015-11-10 01:54:37 +09:00
Junegunn Choi
e7e86b68f4 Add OR operator
Close #412
2015-11-09 23:58:53 +09:00
Junegunn Choi
a89d8995c3 Add execute-multi action
Close #413
2015-11-09 23:58:19 +09:00
Junegunn Choi
dbc854d5f4 Handle wide unicode characters in --prompt 2015-11-09 22:01:40 +09:00
Junegunn Choi
f1cd0e2daf [zsh] Fix #404 - Escape $ in $LBUFFER 2015-11-09 12:06:10 +09:00
Junegunn Choi
90d32bd756 [install] Fix #414 - Respect $ZDOTDIR 2015-11-09 01:48:55 +09:00
Junegunn Choi
e99731ea85 [shell] Add FZF_ALT_C_COMMAND for ALT-C (#408) 2015-11-08 00:12:12 +09:00
Junegunn Choi
15659ac6e6 Merge pull request #409 from freitass/master
[bash-completion] Add nvim to f_cmds
2015-11-07 00:23:30 +09:00
Leandro Freitas
3ef41845a9 [bash-completion] Add nvim to f_cmds 2015-11-06 11:22:35 -02:00
Junegunn Choi
c84e681581 Merge pull request #403 from JackDanger/not-relying-on-exit-status-for-ctrl-r
Not relying on exit status for CTRL-R

Patch submitted by @robinro and @JackDanger
Close #403 #242 #241 #203
2015-11-06 08:38:36 +09:00
Jack Danger Canty
c3cf3427b1 Not relying on exit status for CTRL-R
In the case that fzf-tmux returns a user-selected result but with a
non-zero exit status (which can happen if a function inside $PS1 returns
non-zero) this allows CTRL-R to continue working as expected.

Addresses #203 (Tranquility's comment)
2015-11-05 10:08:54 -08:00
Junegunn Choi
2c4f71d85b [zsh] fzf-history-widget - update local declaration 2015-11-04 21:57:18 +09:00
Junegunn Choi
c6328affae Update extended-search mode section of README 2015-11-04 03:16:36 +09:00
Junegunn Choi
aaef18295d Update FZF_DEFAULT_COMMAND example 2015-11-04 03:14:38 +09:00
Junegunn Choi
14f0d2035e Update Homebrew instructions 2015-11-04 03:13:22 +09:00
25 changed files with 474 additions and 156 deletions

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "src/vendor/github.com/junegunn/go-shellwords"]
path = src/vendor/github.com/junegunn/go-shellwords
url = https://github.com/junegunn/go-shellwords.git
[submodule "src/vendor/github.com/junegunn/go-runewidth"]
path = src/vendor/github.com/junegunn/go-runewidth
url = https://github.com/junegunn/go-runewidth.git

View File

@@ -1,6 +1,20 @@
CHANGELOG CHANGELOG
========= =========
0.11.1
------
- Added `--tabstop=SPACES` option
0.11.0
------
- Added OR operator for extended-search mode
- Added `--execute-multi` action
- Fixed incorrect cursor position when unicode wide characters are used in
`--prompt`
- Fixes and improvements in shell extensions
0.10.9 0.10.9
------ ------

View File

@@ -50,10 +50,10 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
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.
```sh ```sh
brew reinstall --HEAD fzf brew install fzf
# Install shell extensions # Install shell extensions
/usr/local/Cellar/fzf/HEAD/install /usr/local/opt/fzf/install
``` ```
#### Install as Vim plugin #### Install as Vim plugin
@@ -78,7 +78,7 @@ while. Please follow the instruction below depending on the installation
method. method.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew reinstall --HEAD fzf` - brew: `brew update; brew reinstall fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
Usage Usage
@@ -108,32 +108,41 @@ vim $(fzf)
- 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
#### Extended-search mode #### Search syntax
Since 0.10.9, fzf starts in "extended-search mode" by default. 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
!rmx`
In this mode, you can specify multiple patterns delimited by spaces, | Token | Match type | Description |
such as: `^music .mp3$ sbtrkt !rmx` | -------- | -------------------- | -------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| Token | Description | Match type | | `^music` | prefix-exact-match | Items that start with `music` |
| -------- | -------------------------------- | -------------------- | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `^music` | Items that start with `music` | prefix-exact-match | | `'wild` | exact-match (quoted) | Items that include `wild` |
| `.mp3$` | Items that end with `.mp3` | suffix-exact-match | | `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` |
| `sbtrkt` | Items that match `sbtrkt` | fuzzy-match | | `!'fire` | inverse-exact-match | Items that do not include `fire` |
| `!rmx` | Items that do not match `rmx` | inverse-fuzzy-match |
| `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
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,
`'`-prefix "unquotes" the term. `'`-prefix "unquotes" the term.
A single bar character term acts as an OR operator. For example, the following
query matches entries that start with `core` and end with either `go`, `rb`,
or `py`.
```
^core go$ | rb$ | py$
```
#### Environment variables #### Environment variables
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'`
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
Examples Examples
-------- --------
@@ -252,6 +261,17 @@ export FZF_COMPLETION_TRIGGER='~~'
export FZF_COMPLETION_OPTS='+c -x' export FZF_COMPLETION_OPTS='+c -x'
``` ```
#### Supported commands
On bash, fuzzy completion is enabled only for a predefined set of commands
(`complete | grep _fzf` to see the list). But you can enable it for other
commands as well like follows.
```sh
# There are also _fzf_path_completion and _fzf_dir_completion
complete -F _fzf_file_completion -o default -o bashdefault doge
```
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
@@ -335,10 +355,10 @@ filtering:
```sh ```sh
# Feed the output of ag into fzf # Feed the output of ag into fzf
ag -l -g "" | fzf ag -g "" | fzf
# Setting ag as the default source for fzf # Setting ag as the default source for fzf
export FZF_DEFAULT_COMMAND='ag -l -g ""' export FZF_DEFAULT_COMMAND='ag -g ""'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use ag instead of find
fzf fzf

4
fzf
View File

@@ -370,7 +370,7 @@ class FZF
+i Case-sensitive match +i Case-sensitive match
-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 the item using index expressions for search
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
@@ -396,7 +396,7 @@ class FZF
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ FZF_DEFAULT_OPTS Default options (e.g. "-x -m --sort 10000")] + $/ + $/
exit x exit x
end end

24
install
View File

@@ -2,25 +2,28 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.10.9 pre=1 || [[ "$@" =~ --pre ]] && version=0.11.1 pre=1 ||
version=0.10.9 pre=0 version=0.11.1 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=1 update_config=1
binary_arch=
help() { help() {
cat << EOF cat << EOF
usage: $0 [OPTIONS] usage: $0 [OPTIONS]
--help Show this message --help Show this message
--bin Download fzf binary only --bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
--all Download fzf binary and update configuration files --all Download fzf binary and update configuration files
to enable key bindings and fuzzy completion to enable key bindings and fuzzy completion
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
--[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]completion Enable/disable fuzzy completion (bash & zsh)
--[no-]update-rc Whether or not to update shell configuration files --[no-]update-rc Whether or not to update shell configuration files
--32 Download 32-bit binary
--64 Download 64-bit binary
EOF EOF
} }
@@ -41,6 +44,8 @@ for opt in $@; do
--no-completion) auto_completion=0 ;; --no-completion) auto_completion=0 ;;
--update-rc) update_config=1 ;; --update-rc) update_config=1 ;;
--no-update-rc) update_config=0 ;; --no-update-rc) update_config=0 ;;
--32) binary_arch=386 ;;
--64) binary_arch=amd64 ;;
--bin) ;; --bin) ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
@@ -143,10 +148,10 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ x86_64) download fzf-$version-darwin_amd64 ;; Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
Darwin\ i*86) download fzf-$version-darwin_386 ;; Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;;
Linux\ x86_64) download fzf-$version-linux_amd64 ;; Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;;
Linux\ i*86) download fzf-$version-linux_386 ;; Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;;
*) binary_available=0 binary_error=1 ;; *) binary_available=0 binary_error=1 ;;
esac esac
@@ -342,7 +347,8 @@ append_line() {
echo echo
for shell in bash zsh; do for shell in bash zsh; do
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}"
done done
if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then
@@ -353,7 +359,7 @@ fi
cat << EOF cat << EOF
Finished. Restart your shell or reload config file. Finished. Restart your shell or reload config file.
source ~/.bashrc # bash source ~/.bashrc # bash
source ~/.zshrc # zsh source ${ZDOTDIR:-~}/.zshrc # zsh
EOF EOF
[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF [ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF

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 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder" .TH fzf 1 "Dec 2015" "fzf 0.11.1" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -157,6 +157,9 @@ e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR \fBfzf --margin 1,5%\fR
.RE .RE
.TP .TP
.BI "--tabstop=" SPACES
Number of spaces for a tab character (default: 8)
.TP
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
@@ -223,6 +226,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -276,6 +280,12 @@ 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.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the double-quoted strings of the
selected entries separated by spaces.
.RE .RE
.TP .TP
.BI "--history=" "HISTORY_FILE" .BI "--history=" "HISTORY_FILE"
@@ -394,6 +404,13 @@ If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that
when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term.
.SS OR operator
A single bar character term acts as an OR operator. For example, the following
query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
\fBrb\fR, or \fBpy\fR.
e.g. \fB^core go$ | rb$ | py$\fR
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -241,9 +241,9 @@ complete -o default -F _fzf_opts_completion fzf
d_cmds="cd pushd rmdir" d_cmds="cd pushd rmdir"
f_cmds=" f_cmds="
awk cat diff diff3 awk cat diff diff3
emacs ex file ftp g++ gcc gvim head hg java emacs emacsclient ex file ftp g++ gcc gvim head hg java
javac ld less more mvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc" sed sftp sort source tail tee uniq vi view vim wc xdg-open"
a_cmds=" a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar find git grep gunzip gzip hg jar

View File

@@ -21,7 +21,7 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
if ! setopt | grep nonomatch > /dev/null; then if ! setopt | \grep nonomatch > /dev/null; then
nnm=1 nnm=1
setopt nonomatch setopt nonomatch
fi fi
@@ -116,11 +116,8 @@ _fzf_complete_unalias() {
} }
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
if setopt | grep shwordsplit > /dev/null; then setopt localoptions noshwordsplit
sws=1
unsetopt shwordsplit
fi
# 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
@@ -163,12 +160,10 @@ fzf-completion() {
else else
eval "zle ${fzf_default_completion:-expand-or-complete}" eval "zle ${fzf_default_completion:-expand-or-complete}"
fi fi
[ -n "$sws" ] && setopt shwordsplit
} }
[ -z "$fzf_default_completion" ] && [ -z "$fzf_default_completion" ] &&
fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}') fzf_default_completion=$(bindkey '^I' | \grep -v undefined-key | awk '{print $2}')
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion

View File

@@ -1,7 +1,7 @@
# 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 . \\( -path '*/\\.*' -o -fstype 'dev' -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 | sed 1d | cut -b3-"}"
@@ -29,9 +29,10 @@ __fzf_select_tmux__() {
} }
__fzf_cd__() { __fzf_cd__() {
local dir local cmd dir
dir=$(command \find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
dir=$(eval "$cmd" | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
__fzf_history__() ( __fzf_history__() (

View File

@@ -33,9 +33,11 @@ function fzf_key_bindings
end end
function __fzf_alt_c function __fzf_alt_c
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"
# Fish hangs if the command before pipe redirects (2> /dev/null) # Fish hangs if the command before pipe redirects (2> /dev/null)
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m > $TMPDIR/fzf.result"
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result) and cd (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint

View File

@@ -4,7 +4,7 @@ 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 . \\( -path '*/\\.*' -o -fstype 'dev' -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 | sed 1d | cut -b3-"}"
@@ -27,8 +27,9 @@ 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 \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
cd "${$(eval "$cmd" | $(__fzfcmd) +m):-.}"
zle reset-prompt zle reset-prompt
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -36,8 +37,9 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected restore_no_bang_hist local selected num
if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "${LBUFFER//$/\\$}") )
if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
zle vi-fetch-history -n $num zle vi-fetch-history -n $num

44
src/Dockerfile.android Normal file
View File

@@ -0,0 +1,44 @@
FROM ubuntu:14.04
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 && \
sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c
ENV GOPATH /go
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
RUN cd / && \
curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \
chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \
mv android-ndk-r10e /android-ndk
RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm
ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc
RUN cd $GOROOT/src && \
CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash
RUN cd / && curl \
http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \
tar -xz && cd /ncurses-5.9 && \
./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \
sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \
make && \
sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \
make install && \
mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a
# Volume
VOLUME /go
# Default CMD
CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash

View File

@@ -11,9 +11,16 @@ 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
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | \
tar -xz && mv go go1.5
ENV GO15VENDOREXPERIMENT 1
ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.5
ENV GOPATH /go ENV GOPATH /go
ENV GOROOT /go1.4 ENV PATH /go1.5/bin:$PATH
ENV PATH /go1.4/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

@@ -2,12 +2,14 @@ ifndef GOPATH
$(error GOPATH is undefined) $(error GOPATH is undefined)
endif endif
ifndef GOOS
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
GOOS := darwin GOOS := darwin
else ifeq ($(UNAME_S),Linux) else ifeq ($(UNAME_S),Linux)
GOOS := linux GOOS := linux
endif endif
endif
ifneq ($(shell uname -m),x86_64) ifneq ($(shell uname -m),x86_64)
$(error "Build on $(UNAME_M) is not supported, yet.") $(error "Build on $(UNAME_M) is not supported, yet.")
@@ -16,11 +18,13 @@ endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
BINDIR := ../bin BINDIR := ../bin
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
VERSION = $(shell fzf/$(BINARY64) --version) BINARYARM7 := fzf-$(GOOS)_arm7
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 RELEASE32 = fzf-$(VERSION)-$(GOOS)_386
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM7 = fzf-$(VERSION)-$(GOOS)_arm7
all: release all: release
@@ -31,9 +35,14 @@ release: build
build: test fzf/$(BINARY32) fzf/$(BINARY64) build: test fzf/$(BINARY32) fzf/$(BINARY64)
android-build:
cd fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7)
cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7)
test: test:
go get GO15VENDOREXPERIMENT=1 go get
go test -v ./... GO15VENDOREXPERIMENT=1 SHELL=/bin/sh go test -v ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
@@ -44,10 +53,10 @@ clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): $(SOURCES) fzf/$(BINARY32): $(SOURCES)
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) cd fzf && GO15VENDOREXPERIMENT=1 GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32)
fzf/$(BINARY64): $(SOURCES) fzf/$(BINARY64): $(SOURCES)
cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) cd fzf && GO15VENDOREXPERIMENT=1 go build -a -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR)
@@ -65,6 +74,9 @@ docker-ubuntu:
docker-centos: docker-centos:
docker build -t junegunn/centos-sandbox - < Dockerfile.centos docker build -t junegunn/centos-sandbox - < Dockerfile.centos
docker-android:
docker build -t junegunn/android-sandbox - < Dockerfile.android
arch: docker-arch arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
@@ -81,5 +93,14 @@ linux: docker-centos
docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static'
ubuntu-android: docker-android
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
android: docker-android
docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; GOOS=android make android-build'
.PHONY: all build release test install uninstall clean docker \ .PHONY: all build release test install uninstall clean docker \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.10.9" version = "0.11.1"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -5,6 +5,12 @@ package curses
#include <locale.h> #include <locale.h>
#cgo !static LDFLAGS: -lncurses #cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl #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 "C"
@@ -260,7 +266,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.newterm(nil, C.stderr, C.stdin) _screen = C.c_newterm()
if _screen == nil { if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2) os.Exit(2)

View File

@@ -63,6 +63,9 @@ func (item *Item) Rank(cache bool) Rank {
matchlen += end - begin matchlen += end - begin
} }
} }
if matchlen == 0 {
matchlen = math.MaxUint16
}
var tiebreak uint16 var tiebreak uint16
switch rankTiebreak { switch rankTiebreak {
case byLength: case byLength:

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"math"
"sort" "sort"
"testing" "testing"
@@ -42,7 +43,7 @@ func TestItemRank(t *testing.T) {
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { if rank1.matchlen != math.MaxUint16 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
@@ -68,9 +69,9 @@ func TestItemRank(t *testing.T) {
item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 || if items[0] != &item6 || items[1] != &item4 ||
items[2] != &item6 || items[3] != &item4 || items[2] != &item5 || items[3] != &item3 ||
items[4] != &item5 || items[5] != &item3 { items[4] != &item2 || items[5] != &item1 {
t.Error(items) t.Error(items)
} }
} }

View File

@@ -22,7 +22,7 @@ const usage = `usage: fzf [options]
+i Case-sensitive match +i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform item using index expressions within finder --with-nth=N[,..] Transform 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)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
@@ -38,6 +38,7 @@ const usage = `usage: fzf [options]
--black Use black background --black Use black background
--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)
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
@@ -59,7 +60,7 @@ const usage = `usage: fzf [options]
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info') FZF_DEFAULT_OPTS Default options (e.g. '--reverse --inline-info')
` `
@@ -123,6 +124,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]string Margin [4]string
Tabstop int
Version bool Version bool
} }
@@ -169,6 +171,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
Margin: defaultMargin(), Margin: defaultMargin(),
Tabstop: 8,
Version: false} Version: false}
} }
@@ -466,10 +469,13 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
} }
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" if strings.HasPrefix(src, ":execute-multi") {
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
}
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
@@ -565,11 +571,18 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
toggleSort = true toggleSort = true
default: default:
if isExecuteAction(actLower) { if isExecuteAction(actLower) {
keymap[key] = actExecute var offset int
if act[7] == ':' { if strings.HasPrefix(actLower, "execute-multi") {
execmap[key] = act[8:] keymap[key] = actExecuteMulti
offset = len("execute-multi")
} else { } else {
execmap[key] = act[8 : len(act)-1] keymap[key] = actExecute
offset = len("execute")
}
if act[offset] == ':' {
execmap[key] = act[offset+1:]
} else {
execmap[key] = act[offset+1 : len(act)-1]
} }
} else { } else {
errorExit("unknown action: " + act) errorExit("unknown action: " + act)
@@ -580,10 +593,16 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) bool {
if !strings.HasPrefix(str, "execute") || len(str) < 9 { if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") {
return false return false
} }
b := str[7] b := str[len("execute")]
if strings.HasPrefix(str, "execute-multi") {
if len(str) < len("execute-multi()") {
return false
}
b = str[len("execute-multi")]
}
e := str[len(str)-1] e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
@@ -806,6 +825,8 @@ func parseOptions(opts *Options, allArgs []string) {
case "--margin": case "--margin":
opts.Margin = parseMargin( opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -845,6 +866,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match { } else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
@@ -855,6 +878,10 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("header lines must be a non-negative integer") errorExit("header lines must be a non-negative integer")
} }
if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer")
}
// Change default actions for CTRL-N / CTRL-P when --history is used // Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil { if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs { if _, prs := keymap[curses.CtrlP]; !prs {

View File

@@ -36,6 +36,8 @@ type term struct {
origText []rune origText []rune
} }
type termSet []term
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
fuzzy bool fuzzy bool
@@ -43,8 +45,8 @@ type Pattern struct {
caseSensitive bool caseSensitive bool
forward bool forward bool
text []rune text []rune
terms []term termSets []termSet
hasInvTerm bool cacheable bool
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, bool, []rune, []rune) (int, int) procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
@@ -88,14 +90,20 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return cached return cached
} }
caseSensitive, hasInvTerm := true, false caseSensitive, cacheable := true, true
terms := []term{} termSets := []termSet{}
if extended { if extended {
terms = parseTerms(fuzzy, caseMode, asString) termSets = parseTerms(fuzzy, caseMode, asString)
for _, term := range terms { Loop:
if term.inv { for _, termSet := range termSets {
hasInvTerm = true for idx, term := range termSet {
// If the query contains inverse search terms or OR operators,
// we cannot cache the search scope
if idx > 0 || term.inv {
cacheable = false
break Loop
}
} }
} }
} else { } else {
@@ -113,8 +121,8 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
forward: forward, forward: forward,
text: []rune(asString), text: []rune(asString),
terms: terms, termSets: termSets,
hasInvTerm: hasInvTerm, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))}
@@ -129,9 +137,11 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return ptr return ptr
} }
func parseTerms(fuzzy bool, caseMode Case, str string) []term { func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
terms := []term{} sets := []termSet{}
set := termSet{}
switchSet := false
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, token typ, inv, text := termFuzzy, false, token
lowerText := strings.ToLower(text) lowerText := strings.ToLower(text)
@@ -145,6 +155,11 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []term {
typ = termExact typ = termExact
} }
if text == "|" {
switchSet = false
continue
}
if strings.HasPrefix(text, "!") { if strings.HasPrefix(text, "!") {
inv = true inv = true
text = text[1:] text = text[1:]
@@ -173,15 +188,23 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []term {
} }
if len(text) > 0 { if len(text) > 0 {
terms = append(terms, term{ if switchSet {
sets = append(sets, set)
set = termSet{}
}
set = append(set, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: []rune(text),
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
origText: origText}) origText: origText})
switchSet = true
} }
} }
return terms if len(set) > 0 {
sets = append(sets, set)
}
return sets
} }
// IsEmpty returns true if the pattern is effectively empty // IsEmpty returns true if the pattern is effectively empty
@@ -189,7 +212,7 @@ func (p *Pattern) IsEmpty() bool {
if !p.extended { if !p.extended {
return len(p.text) == 0 return len(p.text) == 0
} }
return len(p.terms) == 0 return len(p.termSets) == 0
} }
// AsString returns the search query in string type // AsString returns the search query in string type
@@ -203,11 +226,10 @@ func (p *Pattern) CacheKey() string {
return p.AsString() return p.AsString()
} }
cacheableTerms := []string{} cacheableTerms := []string{}
for _, term := range p.terms { for _, termSet := range p.termSets {
if term.inv { if len(termSet) == 1 && !termSet[0].inv {
continue cacheableTerms = append(cacheableTerms, string(termSet[0].origText))
} }
cacheableTerms = append(cacheableTerms, string(term.origText))
} }
return strings.Join(cacheableTerms, " ") return strings.Join(cacheableTerms, " ")
} }
@@ -218,7 +240,7 @@ func (p *Pattern) Match(chunk *Chunk) []*Item {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if !p.hasInvTerm { // Because we're excluding Inv-term from cache key if p.cacheable {
if cached, found := _cache.Find(chunk, cacheKey); found { if cached, found := _cache.Find(chunk, cacheKey); found {
return cached return cached
} }
@@ -243,7 +265,7 @@ Loop:
matches := p.matchChunk(space) matches := p.matchChunk(space)
if !p.hasInvTerm { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
} }
return matches return matches
@@ -260,7 +282,7 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
} }
} else { } else {
for _, item := range *chunk { for _, item := range *chunk {
if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) { if offsets := p.extendedMatch(item); len(offsets) == len(p.termSets) {
matches = append(matches, dupItem(item, offsets)) matches = append(matches, dupItem(item, offsets))
} }
} }
@@ -275,7 +297,7 @@ func (p *Pattern) MatchItem(item *Item) bool {
return sidx >= 0 return sidx >= 0
} }
offsets := p.extendedMatch(item) offsets := p.extendedMatch(item)
return len(offsets) == len(p.terms) return len(offsets) == len(p.termSets)
} }
func dupItem(item *Item, offsets []Offset) *Item { func dupItem(item *Item, offsets []Offset) *Item {
@@ -301,15 +323,23 @@ func (p *Pattern) basicMatch(item *Item) (int, int, int) {
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) []Offset {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, termSet := range p.termSets {
pfun := p.procFun[term.typ] var offset *Offset
if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { for _, term := range termSet {
if term.inv { pfun := p.procFun[term.typ]
if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
if term.inv {
continue
}
offset = &Offset{int32(sidx), int32(eidx), int32(tlen)}
break break
} else if term.inv {
offset = &Offset{0, 0, 0}
continue
} }
offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) }
} else if term.inv { if offset != nil {
offsets = append(offsets, Offset{0, 0, 0}) offsets = append(offsets, *offset)
} }
} }
return offsets return offsets

View File

@@ -9,20 +9,25 @@ import (
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(true, CaseSmart, terms := parseTerms(true, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |")
if len(terms) != 9 || if len(terms) != 9 ||
terms[0].typ != termFuzzy || terms[0].inv || terms[0][0].typ != termFuzzy || terms[0][0].inv ||
terms[1].typ != termExact || terms[1].inv || terms[1][0].typ != termExact || terms[1][0].inv ||
terms[2].typ != termPrefix || terms[2].inv || terms[2][0].typ != termPrefix || terms[2][0].inv ||
terms[3].typ != termSuffix || terms[3].inv || terms[3][0].typ != termSuffix || terms[3][0].inv ||
terms[4].typ != termFuzzy || !terms[4].inv || terms[4][0].typ != termFuzzy || !terms[4][0].inv ||
terms[5].typ != termExact || !terms[5].inv || terms[5][0].typ != termExact || !terms[5][0].inv ||
terms[6].typ != termPrefix || !terms[6].inv || terms[6][0].typ != termPrefix || !terms[6][0].inv ||
terms[7].typ != termSuffix || !terms[7].inv || terms[7][0].typ != termSuffix || !terms[7][0].inv ||
terms[8].typ != termEqual || terms[8].inv { terms[7][1].typ != termEqual || terms[7][1].inv ||
terms[8][0].typ != termPrefix || terms[8][0].inv ||
terms[8][1].typ != termExact || terms[8][1].inv ||
terms[8][2].typ != termSuffix || terms[8][2].inv ||
terms[8][3].typ != termFuzzy || !terms[8][3].inv {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
for idx, term := range terms { for idx, termSet := range terms[:8] {
term := termSet[0]
if len(term.text) != 3 { if len(term.text) != 3 {
t.Errorf("%s", term) t.Errorf("%s", term)
} }
@@ -30,20 +35,25 @@ func TestParseTermsExtended(t *testing.T) {
t.Errorf("%s", term) t.Errorf("%s", term)
} }
} }
for _, term := range terms[8] {
if len(term.origText) != 4 {
t.Errorf("%s", term)
}
}
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(false, CaseSmart, terms := parseTerms(false, CaseSmart,
"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].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 ||
terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 || terms[1][0].typ != termFuzzy || terms[1][0].inv || len(terms[1][0].text) != 3 ||
terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || terms[2][0].typ != termPrefix || terms[2][0].inv || len(terms[2][0].text) != 3 ||
terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || terms[3][0].typ != termSuffix || terms[3][0].inv || len(terms[3][0].text) != 3 ||
terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || terms[4][0].typ != termExact || !terms[4][0].inv || len(terms[4][0].text) != 3 ||
terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 || terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 ||
terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 ||
terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
} }
@@ -61,9 +71,9 @@ func TestExact(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, pattern := BuildPattern(true, true, CaseSmart, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
sidx, eidx := algo.ExactMatchNaive( sidx, eidx := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text)
if sidx != 7 || eidx != 10 { if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx)
} }
} }
@@ -74,9 +84,9 @@ func TestEqual(t *testing.T) {
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
sidx, eidx := algo.EqualMatch( sidx, eidx := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text)
if sidx != sidxExpected || eidx != eidxExpected { if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx)
} }
} }
match("ABC", -1, -1) match("ABC", -1, -1)
@@ -130,3 +140,25 @@ func TestOrigTextAndTransformed(t *testing.T) {
} }
} }
} }
func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if pat.cacheable != cacheable {
t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr)
}
clearPatternCache()
}
test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true)
test(true, "foo bar baz", "foo bar baz", true)
test(true, "foo !bar", "foo", false)
test(true, "foo !bar baz", "foo baz", false)
test(true, "foo | bar baz", "baz", false)
test(true, "foo | bar | baz", "", false)
test(true, "foo | bar !baz", "", false)
test(true, "| | | foo", "foo", true)
}

View File

@@ -80,6 +80,7 @@ func (a byTimeOrder) Less(i, j int) bool {
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int) var _runeWidths = make(map[rune]int)
var _tabStop int
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
@@ -132,6 +133,7 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti
) )
func defaultKeymap() map[int]actionType { func defaultKeymap() map[int]actionType {
@@ -193,6 +195,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else { } else {
header = reverseStringArray(opts.Header) header = reverseStringArray(opts.Header)
} }
_tabStop = opts.Tabstop
return &Terminal{ return &Terminal{
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
prompt: opts.Prompt, prompt: opts.Prompt,
@@ -305,21 +308,25 @@ func (t *Terminal) output() bool {
found = true found = true
} }
} else { } else {
sels := make([]selectedItem, 0, len(t.selected)) for _, sel := range t.sortSelected() {
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text) fmt.Println(*sel.text)
} }
} }
return found return found
} }
func (t *Terminal) sortSelected() []selectedItem {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
return sels
}
func runeWidth(r rune, prefixWidth int) int { func runeWidth(r rune, prefixWidth int) int {
if r == '\t' { if r == '\t' {
return 8 - prefixWidth%8 return _tabStop - prefixWidth%_tabStop
} else if w, found := _runeWidths[r]; found { } else if w, found := _runeWidths[r]; found {
return w return w
} else { } else {
@@ -391,7 +398,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
} }
func (t *Terminal) placeCursor() { func (t *Terminal) placeCursor() {
t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false)
} }
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
@@ -402,7 +409,7 @@ func (t *Terminal) printPrompt() {
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading { if t.reading {
C.CPrint(C.ColSpinner, true, " < ") C.CPrint(C.ColSpinner, true, " < ")
} else { } else {
@@ -698,8 +705,12 @@ 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) { func quoteEntry(entry string) string {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1) return fmt.Sprintf("%q", entry)
}
func executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1)
cmd := exec.Command("sh", "-c", command) cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -858,7 +869,17 @@ func (t *Terminal) Loop() {
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
}
case actExecuteMulti:
if len(t.selected) > 0 {
sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text)
}
executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else {
return doAction(actExecute, mapkey)
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
@@ -1023,7 +1044,7 @@ func (t *Terminal) Loop() {
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3] mx -= t.marginInt[3]
my -= t.marginInt[0] my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse { if !t.reverse {
my = t.maxHeight() - my - 1 my = t.maxHeight() - my - 1
} }

View File

@@ -6,6 +6,7 @@ require 'fileutils'
DEFAULT_TIMEOUT = 20 DEFAULT_TIMEOUT = 20
FILE = File.expand_path(__FILE__)
base = File.expand_path('../../', __FILE__) base = File.expand_path('../../', __FILE__)
Dir.chdir base Dir.chdir base
FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf"
@@ -713,6 +714,24 @@ class TestGoFZF < TestBase
File.unlink output rescue nil File.unlink output rescue nil
end end
def test_execute_multi
output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a
tmux.send_keys :BTab, :BTab, :BTab
tmux.send_keys :Escape, :a
tmux.send_keys :Tab, :Tab
tmux.send_keys :Escape, :a
tmux.send_keys :Enter
readonce
assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'],
File.readlines(output).map(&:chomp)
ensure
File.unlink output rescue nil
end
def test_cycle def test_cycle
tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
tmux.until { |lines| lines[-2].include? '8/8' } tmux.until { |lines| lines[-2].include? '8/8' }
@@ -785,8 +804,8 @@ class TestGoFZF < TestBase
end end
def test_header def test_header
tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{FILE})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[-2].include?('100/100') && lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header lines[-7..-3].map(&:strip) == header
@@ -794,8 +813,8 @@ class TestGoFZF < TestBase
end end
def test_header_reverse def test_header_reverse
tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{FILE})\\\" --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[1].include?('100/100') && lines[1].include?('100/100') &&
lines[2..6].map(&:strip) == header lines[2..6].map(&:strip) == header
@@ -803,8 +822,8 @@ class TestGoFZF < TestBase
end end
def test_header_and_header_lines def test_header_and_header_lines
tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[-2].include?('90/90') && lines[-2].include?('90/90') &&
lines[-7...-2].map(&:strip) == header && lines[-7...-2].map(&:strip) == header &&
@@ -813,8 +832,8 @@ class TestGoFZF < TestBase
end end
def test_header_and_header_lines_reverse def test_header_and_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(FILE).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[1].include?('90/90') && lines[1].include?('90/90') &&
lines[2...7].map(&:strip) == header && lines[2...7].map(&:strip) == header &&
@@ -847,6 +866,26 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_tabstop
writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"]
{
1 => '> f oo ba r baz barfooq ux',
2 => '> f oo ba r baz barfooq ux',
3 => '> f oo ba r baz barfooq ux',
4 => '> f oo ba r baz barfooq ux',
5 => '> f oo ba r baz barfooq ux',
6 => '> f oo ba r baz barfooq ux',
7 => '> f oo ba r baz barfooq ux',
8 => '> f oo ba r baz barfooq ux',
9 => '> f oo ba r baz barfooq ux',
}.each do |ts, exp|
tmux.prepare
tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter
tmux.until { |lines| lines[-3] == exp }
tmux.send_keys :Enter
end
end
def test_with_nth def test_with_nth
writelines tempname, ['hello world ', 'byebye'] writelines tempname, ['hello world ', 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
@@ -915,6 +954,12 @@ class TestGoFZF < TestBase
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
end end
def test_or_operator
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines.map(&:chomp)
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
`seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp)
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
@@ -981,6 +1026,22 @@ module TestShell
tmux.until { |lines| lines[-1].end_with?(expected) } tmux.until { |lines| lines[-1].end_with?(expected) }
end end
def test_alt_c_command
set_var 'FZF_ALT_C_COMMAND', 'echo /tmp'
tmux.prepare
tmux.send_keys 'cd /', :Enter
tmux.prepare
tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until(1) { |lines| lines.item_count == 1 }
tmux.send_keys :Enter, pane: 1
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| lines[-1].end_with? '/tmp' }
end
def test_ctrl_r def test_ctrl_r
tmux.prepare tmux.prepare
tmux.send_keys 'echo 1st', :Enter; tmux.prepare tmux.send_keys 'echo 1st', :Enter; tmux.prepare