Compare commits

...

35 Commits

Author SHA1 Message Date
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
Junegunn Choi
64afff6b9a 0.10.9 2015-11-03 23:03:49 +09:00
Junegunn Choi
6bddffbca4 Setup signal handlers before ncurses initialization
This prevents fzf from missing SIGWINCH during startup which
occasionally happens with fzf-tmux
2015-11-03 23:00:34 +09:00
Junegunn Choi
81a88693c1 Make --extended default
Close #400
2015-11-03 22:49:32 +09:00
Junegunn Choi
68541e66b7 [man] double-click for --bind (#374) 2015-11-03 22:40:45 +09:00
Junegunn Choi
672b593634 Update FZF_DEFAULT_COMMAND example (#310) 2015-11-03 22:25:50 +09:00
Junegunn Choi
5769d3867d [nvim] setf fzf 2015-10-31 00:18:23 +09:00
Junegunn Choi
724ffa3756 [install] Do not download binary if it's found in $PATH (#373)
/cc @xconstruct
2015-10-26 12:31:43 +09:00
Junegunn Choi
5694b5ed30 Fix #394 - --bin option is broken 2015-10-23 17:43:34 +09:00
Junegunn Choi
a1184ceb4e Fix travis CI build 2015-10-23 15:07:16 +09:00
Junegunn Choi
02203c7739 Add command-line flags to install script
Close #392

  usage: ./install [OPTIONS]

      --help               Show this message
      --bin                Download fzf binary only
      --all                Download fzf binary and update configuration files
                           to enable key bindings and fuzzy completion
      --[no-]key-bindings  Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
      --[no-]completion    Enable/disable fuzzy completion (bash & zsh)
      --[no-]update-rc     Whether or not to update shell configuration files
2015-10-23 15:04:32 +09:00
Junegunn Choi
4d709e0dd2 Fix #391 - Strip non-printable characters 2015-10-23 01:12:31 +09:00
Junegunn Choi
ae04f56dbd Fix --bind "double-click:execute(...)" (#374) 2015-10-13 02:36:11 +09:00
Junegunn Choi
f80ff8c917 Add bindable double-click event (#374) 2015-10-13 02:24:38 +09:00
Junegunn Choi
b4ce89bbf5 [build] Link libncursesw when building 64-bit linux binary
Close #376
2015-10-12 16:02:08 +09:00
Junegunn Choi
486b87d821 [bash-completion] Retain original completion options (#288) 2015-10-12 00:27:30 +09:00
Junegunn Choi
b3010a4624 0.10.8 2015-10-09 12:42:07 +09:00
Junegunn Choi
7d53051ec8 Merge pull request #371 from wilywampa/edit_directory
Trigger netrw autocommand when opening directory
2015-10-09 12:36:08 +09:00
Jacob Niehus
ed893c5f47 Trigger netrw autocommand when opening directory 2015-10-08 20:28:07 -07:00
Junegunn Choi
a4eb3323da Fix #370 - Panic when trying to set colors when colors are disabled 2015-10-09 12:16:47 +09:00
20 changed files with 779 additions and 442 deletions

View File

@@ -1,6 +1,30 @@
CHANGELOG CHANGELOG
========= =========
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
------
- Extended-search mode is now enabled by default
- `--extended-exact` is deprecated and instead we have `--exact` for
orthogonally controlling "exactness" of search
- Fixed not to display non-printable characters
- Added `double-click` for `--bind` option
- More robust handling of SIGWINCH
0.10.8
------
- Fixed panic when trying to set colors after colors are disabled (#370)
0.10.7 0.10.7
------ ------

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
@@ -68,7 +68,7 @@ Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
(recommended): (recommended):
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
``` ```
#### Upgrading fzf #### Upgrading fzf
@@ -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
With `-x` or `--extended` option, fzf will start in "extended-search mode". 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 `--extended-exact` option. Note that in start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
`--extended-exact` mode, `'`-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="--extended --cycle"` - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
Examples Examples
-------- --------
@@ -335,10 +344,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
@@ -355,7 +364,8 @@ speed of the traversal.
```sh ```sh
export FZF_DEFAULT_COMMAND=' export FZF_DEFAULT_COMMAND='
(git ls-tree -r --name-only HEAD || (git ls-tree -r --name-only HEAD ||
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' find . -path "*/\.*" -prune -o -type f -print -o -type l -print |
sed s/^..//) 2> /dev/null'
``` ```
#### Fish shell #### Fish shell

136
install
View File

@@ -1,12 +1,60 @@
#!/usr/bin/env bash #!/usr/bin/env bash
[[ "$@" =~ --pre ]] && version=0.10.7 pre=1 || set -u
version=0.10.7 pre=0
[[ "$@" =~ --pre ]] && version=0.11.0 pre=1 ||
version=0.11.0 pre=0
auto_completion=
key_bindings=
update_config=1
help() {
cat << EOF
usage: $0 [OPTIONS]
--help Show this message
--bin Download fzf binary only
--all Download fzf binary and update configuration files
to enable key bindings and fuzzy completion
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
--[no-]completion Enable/disable fuzzy completion (bash & zsh)
--[no-]update-rc Whether or not to update shell configuration files
EOF
}
for opt in $@; do
case $opt in
--help)
help
exit 0
;;
--all)
auto_completion=1
key_bindings=1
update_config=1
;;
--key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;;
--completion) auto_completion=1 ;;
--no-completion) auto_completion=0 ;;
--update-rc) update_config=1 ;;
--no-update-rc) update_config=0 ;;
--bin) ;;
*)
echo "unknown option: $opt"
help
exit 1
;;
esac
done
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base="$(pwd)"
# If stdin is a tty, we are "interactive". # If stdin is a tty, we are "interactive".
interactive=
[ -t 0 ] && interactive=yes [ -t 0 ] && interactive=yes
ask() { ask() {
@@ -16,7 +64,7 @@ ask() {
read -p "$1 ([y]/n) " $read_n -r read -p "$1 ([y]/n) " $read_n -r
echo echo
[[ ! $REPLY =~ ^[Nn]$ ]] [[ $REPLY =~ ^[Nn]$ ]]
} }
check_binary() { check_binary() {
@@ -55,9 +103,16 @@ download() {
if [ -x "$fzf_base"/bin/fzf ]; then if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then fi
if [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return symlink $1 && check_binary && return
fi fi
if which_fzf="$(which fzf 2> /dev/null)"; then
echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -173,12 +228,16 @@ fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ "$*" =~ "--bin" ]] && exit 0
# Auto-completion # Auto-completion
ask "Do you want to add auto-completion support?" if [ -z "$auto_completion" ]; then
auto_completion=$? ask "Do you want to enable fuzzy auto-completion?"
auto_completion=$?
fi
# Key-bindings # Key-bindings
ask "Do you want to add key bindings?" if [ -z "$key_bindings" ]; then
key_bindings=$? ask "Do you want to enable key bindings?"
key_bindings=$?
fi
echo echo
for shell in bash zsh; do for shell in bash zsh; do
@@ -186,12 +245,12 @@ for shell in bash zsh; do
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -ne 0 ]; then if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
if [ $key_bindings -ne 0 ]; then if [ $key_bindings -eq 0 ]; then
fzf_key_bindings="# $fzf_key_bindings" fzf_key_bindings="# $fzf_key_bindings"
fi fi
@@ -237,29 +296,45 @@ EOF
rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed" rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
fi fi
if [ $key_bindings -eq 0 ]; then fish_binding=~/.config/fish/functions/fzf_key_bindings.fish
echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... " if [ $key_bindings -ne 0 ]; then
ln -sf $fzf_base/shell/key-bindings.fish \ echo -n "Symlink $fish_binding ... "
~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed" ln -sf "$fzf_base/shell/key-bindings.fish" \
"$fish_binding" && echo "OK" || echo "Failed"
else
echo -n "Removing $fish_binding ... "
rm -f "$fish_binding"
echo "OK"
fi fi
fi fi
append_line() { append_line() {
set -e set -e
echo "Update $2:"
echo " - $1" local skip line file pat lno
[ -f "$2" ] || touch "$2" skip="$1"
if [ $# -lt 3 ]; then line="$2"
line=$(\grep -nF "$1" "$2" | sed 's/:.*//' | tr '\n' ' ') file="$3"
pat="${4:-}"
echo "Update $file:"
echo " - $line"
[ -f "$file" ] || touch "$file"
if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
else else
line=$(\grep -nF "$3" "$2" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
fi fi
if [ -n "$line" ]; then if [ -n "$lno" ]; then
echo " - Already exists: line #$line" echo " - Already exists: line #$lno"
else else
echo >> "$2" if [ $skip -eq 1 ]; then
echo "$1" >> "$2" echo >> "$file"
echo " + Added" echo "$line" >> "$file"
echo " + Added"
else
echo " ~ Skipped"
fi
fi fi
echo echo
set +e set +e
@@ -267,18 +342,19 @@ append_line() {
echo echo
for shell in bash zsh; do for shell in bash zsh; do
append_line "[ -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 0 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file" append_line $update_config "fzf_key_bindings" "$bind_file"
fi 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 "Oct 2015" "fzf 0.10.7" "fzf - a command-line fuzzy finder" .TH fzf 1 "Nov 2015" "fzf 0.11.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -36,10 +36,11 @@ fzf is a general-purpose command-line fuzzy finder.
.SS Search mode .SS Search mode
.TP .TP
.B "-x, --extended" .B "-x, --extended"
Extended-search mode Extended-search mode. Since 0.10.9, this is enabled by default. You can disable
it with \fB+x\fR or \fB--no-extended\fR.
.TP .TP
.B "-e, --extended-exact" .B "-e, --exact"
Extended-search mode (exact match) Enable exact-match
.TP .TP
.B "-i" .B "-i"
Case-insensitive match (default: smart-case match) Case-insensitive match (default: smart-case match)
@@ -179,11 +180,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE .RE
.RS .RS
.B AVAILABLE KEYS: .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIf[1-4]\fR \fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR \fIspace\fR
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-bspace\fR (\fIalt-bs\fR)
@@ -201,13 +202,14 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fIpgdn\fR (\fIpage-down\fR) \fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR \fIshift-left\fR
\fIshift-right\fR \fIshift-right\fR
\fIdouble-click\fR
or any single character or any single character
.RE .RE
.RS .RS
\fBACTION: DEFAULT BINDINGS: \fBACTION: DEFAULT BINDINGS:
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIctrl-m (enter)\fR \fBaccept\fR \fIenter double-click\fR
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
@@ -221,6 +223,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
@@ -274,6 +277,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"
@@ -369,9 +378,9 @@ of field index expressions.
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search Unless specified otherwise, fzf will start in "extended-search mode". In this
mode". In this mode, you can specify multiple patterns delimited by spaces, mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted) .SS Exact-match (quoted)
A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
@@ -387,11 +396,17 @@ with the given string. An anchored-match term is also an exact-match term.
If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
term from the result. term from the result.
.SS Extended-exact mode .SS Exact-match by default
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that
(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term.
mode, \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

@@ -357,6 +357,7 @@ function! s:execute_term(dict, command, temps)
endfunction endfunction
call termopen(a:command, fzf) call termopen(a:command, fzf)
setf fzf
startinsert startinsert
return [] return []
endfunction endfunction
@@ -416,6 +417,9 @@ function! s:cmd_callback(lines) abort
set noautochdir set noautochdir
for item in a:lines for item in a:lines
execute cmd s:escape(item) execute cmd s:escape(item)
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor endfor
finally finally
let &autochdir = autochdir let &autochdir = autochdir

View File

@@ -11,8 +11,8 @@
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/.*-F *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\2=\1;/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
sed 's/[^a-z0-9_= ;]/_/g' awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -22,7 +22,7 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-x --extended -x --extended
-e --extended-exact -e --exact
-i +i -i +i
-n --nth -n --nth
-d --delimiter -d --delimiter
@@ -77,12 +77,12 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig ret orig_cmd local cmd orig_var orig ret orig_cmd
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd"
orig=$(eval "echo \$_fzf_orig_completion_$cmd") orig="${!orig_var##*#}"
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@" $orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
@@ -242,7 +242,7 @@ 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 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"
a_cmds=" a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
@@ -252,32 +252,48 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.9.12' ]; then if [ "$_fzf_completion_loaded" != '0.10.8' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | eval $(complete | \grep '\-F' | \grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.9.12 export _fzf_completion_loaded=0.10.8
fi fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
fi fi
_fzf_defc() {
local cmd func opts orig_var orig
cmd="$1"
func="$2"
opts="$3"
orig_var="_fzf_orig_completion_$cmd"
orig="${!orig_var}"
if [ -n "$orig" ]; then
eval "$(printf "$orig" "$func")"
else
complete -F "$func" $opts "$cmd"
fi
}
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs"
done done
# File # File
for cmd in $f_cmds; do for cmd in $f_cmds; do
complete -F _fzf_file_completion -o default -o bashdefault $cmd _fzf_defc "$cmd" _fzf_file_completion "-o default -o bashdefault"
done done
# Anything # Anything
for cmd in $a_cmds; do for cmd in $a_cmds; do
complete -F _fzf_path_completion -o default -o bashdefault $cmd _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
done done
unset _fzf_defc
# Kill completion # Kill completion
complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill

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

View File

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

View File

@@ -143,7 +143,7 @@ func Run(opts *Options) {
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Tiebreak != byEnd, opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd,
opts.Nth, opts.Delimiter, runes) opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)

View File

@@ -4,13 +4,14 @@ package curses
#include <ncurses.h> #include <ncurses.h>
#include <locale.h> #include <locale.h>
#cgo !static LDFLAGS: -lncurses #cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl #cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
*/ */
import "C" import "C"
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"syscall" "syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -50,6 +51,7 @@ const (
Invalid Invalid
Mouse Mouse
DoubleClick
BTab BTab
BSpace BSpace
@@ -513,7 +515,12 @@ func MoveAndClear(y int, x int) {
} }
func Print(text string) { func Print(text string) {
C.addstr(C.CString(text)) C.addstr(C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
} }
func CPrint(pair int, bold bool, text string) { func CPrint(pair int, bold bool, text string) {

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

@@ -16,7 +16,8 @@ const usage = `usage: fzf [options]
Search Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+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
@@ -58,20 +59,10 @@ 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. '-x -m') FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info')
` `
// Mode denotes the current search mode
type Mode int
// Search modes
const (
ModeFuzzy Mode = iota
ModeExtended
ModeExtendedExact
)
// Case denotes case-sensitivity of search // Case denotes case-sensitivity of search
type Case int type Case int
@@ -98,7 +89,8 @@ func defaultMargin() [4]string {
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Fuzzy bool
Extended bool
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
@@ -143,7 +135,8 @@ func defaultTheme() *curses.ColorTheme {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Fuzzy: true,
Extended: true,
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
@@ -343,6 +336,8 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.SLeft chord = curses.SLeft
case "shift-right": case "shift-right":
chord = curses.SRight chord = curses.SRight
case "double-click":
chord = curses.DoubleClick
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = curses.CtrlA + int(lkey[5]) - 'a'
@@ -380,8 +375,11 @@ func parseTiebreak(str string) tiebreak {
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
dupe := *theme if theme != nil {
return &dupe dupe := *theme
return &dupe
}
return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
@@ -402,7 +400,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
} }
// Color is disabled // Color is disabled
if theme == nil { if theme == nil {
errorExit("colors disabled; cannot customize colors") continue
} }
pair := strings.Split(str, ":") pair := strings.Split(str, ":")
@@ -468,10 +466,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)
@@ -567,11 +568,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)
@@ -582,10 +590,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), "~!@#$%^&*;/|") {
@@ -679,11 +693,17 @@ func parseOptions(opts *Options, allArgs []string) {
case "-h", "--help": case "-h", "--help":
help(exitOk) help(exitOk)
case "-x", "--extended": case "-x", "--extended":
opts.Mode = ModeExtended opts.Extended = true
case "-e", "--extended-exact": case "-e", "--exact":
opts.Mode = ModeExtendedExact opts.Fuzzy = false
case "+x", "--no-extended", "+e", "--no-extended-exact": case "--extended-exact":
opts.Mode = ModeFuzzy // Note that we now don't have --no-extended-exact
opts.Fuzzy = false
opts.Extended = true
case "+x", "--no-extended":
opts.Extended = false
case "+e", "--no-exact":
opts.Fuzzy = true
case "-q", "--query": case "-q", "--query":
opts.Query = nextString(allArgs, &i, "query string required") opts.Query = nextString(allArgs, &i, "query string required")
case "-f", "--filter": case "-f", "--filter":
@@ -868,7 +888,7 @@ func parseOptions(opts *Options, allArgs []string) {
// 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.Extended || len(opts.Nth) == 1 {
for _, r := range opts.Nth { for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis { if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0) opts.Nth = make([]Range, 0)

View File

@@ -100,7 +100,7 @@ func TestIrrelevantNth(t *testing.T) {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %s", opts.Nth)
} }
} }
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} { for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
@@ -316,3 +316,15 @@ func TestColorSpec(t *testing.T) {
t.Errorf("using default colors") t.Errorf("using default colors")
} }
} }
func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme
newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil {
t.Errorf("color is disabled. keep it that way.")
}
newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
if newTheme.Prompt != 13 {
t.Errorf("color should now be enabled and customized")
}
}

View File

@@ -36,14 +36,17 @@ type term struct {
origText []rune origText []rune
} }
type termSet []term
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
mode Mode fuzzy bool
extended bool
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)
@@ -63,7 +66,7 @@ func init() {
func clearPatternCache() { func clearPatternCache() {
// We can uniquely identify the pattern for a given string since // We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running // search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern) _patternCache = make(map[string]*Pattern)
} }
@@ -72,14 +75,13 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, forward bool, func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern { nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
switch mode { if extended {
case ModeExtended, ModeExtendedExact:
asString = strings.Trim(string(runes), " ") asString = strings.Trim(string(runes), " ")
default: } else {
asString = string(runes) asString = string(runes)
} }
@@ -88,18 +90,23 @@ func BuildPattern(mode Mode, caseMode Case, forward bool,
return cached return cached
} }
caseSensitive, hasInvTerm := true, false caseSensitive, cacheable := true, true
terms := []term{} termSets := []termSet{}
switch mode { if extended {
case ModeExtended, ModeExtendedExact: termSets = parseTerms(fuzzy, caseMode, asString)
terms = parseTerms(mode, caseMode, asString) Loop:
for _, term := range terms { for _, termSet := range termSets {
if term.inv { for idx, term := range termSet {
hasInvTerm = true // 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
}
} }
} }
default: } else {
lowerString := strings.ToLower(asString) lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect || caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString caseMode == CaseSmart && lowerString != asString
@@ -109,12 +116,13 @@ func BuildPattern(mode Mode, caseMode Case, forward bool,
} }
ptr := &Pattern{ ptr := &Pattern{
mode: mode, fuzzy: fuzzy,
extended: extended,
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(mode Mode, caseMode Case, forward bool,
return ptr return ptr
} }
func parseTerms(mode Mode, 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)
@@ -141,20 +151,26 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
text = lowerText text = lowerText
} }
origText := []rune(text) origText := []rune(text)
if mode == ModeExtendedExact { if !fuzzy {
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:]
} }
if strings.HasPrefix(text, "'") { if strings.HasPrefix(text, "'") {
if mode == ModeExtended { // Flip exactness
if fuzzy {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else if mode == ModeExtendedExact { } else {
typ = termFuzzy typ = termFuzzy
text = text[1:] text = text[1:]
} }
@@ -172,23 +188,31 @@ func parseTerms(mode Mode, 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
func (p *Pattern) IsEmpty() bool { func (p *Pattern) IsEmpty() bool {
if p.mode == ModeFuzzy { 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
@@ -198,15 +222,14 @@ func (p *Pattern) AsString() string {
// CacheKey is used to build string to be used as the key of result cache // CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string { func (p *Pattern) CacheKey() string {
if p.mode == ModeFuzzy { if !p.extended {
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, " ")
} }
@@ -217,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
} }
@@ -242,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
@@ -250,16 +273,16 @@ Loop:
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{} matches := []*Item{}
if p.mode == ModeFuzzy { if !p.extended {
for _, item := range *chunk { for _, item := range *chunk {
if sidx, eidx, tlen := p.fuzzyMatch(item); sidx >= 0 { if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 {
matches = append(matches, matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}}))
} }
} }
} 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))
} }
} }
@@ -269,12 +292,12 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy { if !p.extended {
sidx, _, _ := p.fuzzyMatch(item) sidx, _, _ := p.basicMatch(item)
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 {
@@ -289,23 +312,34 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }
func (p *Pattern) fuzzyMatch(item *Item) (int, int, int) { func (p *Pattern) basicMatch(item *Item) (int, int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
}
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { 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

@@ -8,21 +8,26 @@ import (
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, 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,26 +35,31 @@ 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(ModeExtendedExact, 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)
} }
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -58,25 +68,25 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, 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)
} }
} }
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
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)
@@ -86,17 +96,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -109,19 +119,19 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{}) tokens := Tokenize([]rune("junegunn"), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi") origRunes := []rune("junegunn.choi")
for _, mode := range []Mode{ModeFuzzy, ModeExtended} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: []rune("junegunn"), text: []rune("junegunn"),
origText: &origRunes, origText: &origRunes,
transformed: trans}, transformed: trans},
} }
pattern.mode = mode pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk)
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
@@ -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

@@ -132,6 +132,7 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti
) )
func defaultKeymap() map[int]actionType { func defaultKeymap() map[int]actionType {
@@ -180,6 +181,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.Rune] = actRune keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse keymap[C.Mouse] = actMouse
keymap[C.DoubleClick] = actAccept
return keymap return keymap
} }
@@ -304,18 +306,22 @@ 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 8 - prefixWidth%8
@@ -390,7 +396,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() {
@@ -401,7 +407,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 {
@@ -697,8 +703,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
@@ -712,21 +722,6 @@ func executeCommand(template string, current string) {
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
{ // Late initialization { // Late initialization
t.mutex.Lock()
t.initFunc()
t.calculateMargins()
t.printPrompt()
t.placeCursor()
C.Refresh()
t.printInfo()
t.printHeader()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(initialDelay)
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
intChan := make(chan os.Signal, 1) intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, os.Kill) signal.Notify(intChan, os.Interrupt, os.Kill)
go func() { go func() {
@@ -743,6 +738,21 @@ func (t *Terminal) Loop() {
} }
}() }()
t.mutex.Lock()
t.initFunc()
t.calculateMargins()
t.printPrompt()
t.placeCursor()
C.Refresh()
t.printInfo()
t.printHeader()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(initialDelay)
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
// Keep the spinner spinning // Keep the spinner spinning
go func() { go func() {
for { for {
@@ -850,6 +860,219 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(actionType, int) bool
doAction = func(action actionType, mapkey int) bool {
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], 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:
t.mutex.Unlock()
return false
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[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:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case actDown:
t.vmove(-1)
req(reqList)
case actUp:
t.vmove(1)
req(reqList)
case actAccept:
req(reqClose)
case actClearScreen:
req(reqRedraw)
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[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:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min--
}
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[C.DoubleClick], C.DoubleClick)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
}
}
}
}
return true
}
action := t.keymap[event.Type] action := t.keymap[event.Type]
mapkey := event.Type mapkey := event.Type
if event.Type == C.Rune { if event.Type == C.Rune {
@@ -858,204 +1081,8 @@ func (t *Terminal) Loop() {
action = act action = act
} }
} }
switch action { if !doAction(action, mapkey) {
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(t.ansi))
}
case actInvalid:
t.mutex.Unlock()
continue continue
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[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:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case actDown:
t.vmove(-1)
req(reqList)
case actUp:
t.vmove(1)
req(reqList)
case actAccept:
req(reqClose)
case actClearScreen:
req(reqRedraw)
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[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:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min--
}
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
}
}
}
} }
changed := string(previousInput) != string(t.input) changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox

View File

@@ -8,7 +8,7 @@ DEFAULT_TIMEOUT = 20
base = File.expand_path('../../', __FILE__) base = File.expand_path('../../', __FILE__)
Dir.chdir base Dir.chdir base
FZF = "#{base}/bin/fzf" FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf"
class NilClass class NilClass
def include? str def include? str
@@ -213,7 +213,7 @@ class TestGoFZF < TestBase
end end
def test_fzf_default_command def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -713,6 +713,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' }
@@ -904,6 +922,23 @@ class TestGoFZF < TestBase
end end
end end
def test_default_extended
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
end
def test_exact
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
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
@@ -970,6 +1005,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