m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 06:43:47 -05:00

Compare commits

...

48 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
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
28 changed files with 958 additions and 482 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,35 @@
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
------
- 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
-------- --------
@@ -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
@@ -355,7 +375,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

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

147
install
View File

@@ -1,12 +1,65 @@
#!/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.1 pre=1 ||
version=0.11.1 pre=0
auto_completion=
key_bindings=
update_config=1
binary_arch=
help() {
cat << EOF
usage: $0 [OPTIONS]
--help Show this message
--bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
--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
--32 Download 32-bit binary
--64 Download 64-bit binary
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 ;;
--32) binary_arch=386 ;;
--64) binary_arch=amd64 ;;
--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 +69,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 +108,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
@@ -88,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
@@ -173,12 +233,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 +250,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 +301,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 "$line" >> "$file"
echo " + Added" echo " + Added"
else
echo " ~ Skipped"
fi
fi fi
echo echo
set +e set +e
@@ -267,18 +347,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 "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
@@ -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)
@@ -156,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
@@ -179,11 +183,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 +205,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 +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
@@ -274,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"
@@ -369,9 +381,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 +399,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
@@ -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
@@ -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

@@ -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.")
@@ -18,9 +20,11 @@ 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
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE32 = fzf-$(VERSION)-$(GOOS)_386
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 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.7" version = "0.11.1"
// 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,20 @@ 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
#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"
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"syscall" "syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -50,6 +57,7 @@ const (
Invalid Invalid
Mouse Mouse
DoubleClick
BTab BTab
BSpace BSpace
@@ -258,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)
@@ -513,7 +521,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,12 +16,13 @@ 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
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
@@ -37,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
@@ -58,20 +60,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 Default 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 +90,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
@@ -131,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
} }
@@ -143,7 +137,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),
@@ -176,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}
} }
@@ -343,6 +339,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 +378,11 @@ func parseTiebreak(str string) tiebreak {
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
if theme != nil {
dupe := *theme dupe := *theme
return &dupe return &dupe
}
return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
@@ -402,7 +403,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 +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)
@@ -567,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)
@@ -582,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), "~!@#$%^&*;/|") {
@@ -679,11 +696,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":
@@ -802,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:
@@ -841,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)
} }
@@ -851,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 {
@@ -868,7 +899,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)
if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
}
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 {
var offset *Offset
for _, term := range termSet {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
if term.inv { if term.inv {
break continue
} }
offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) offset = &Offset{int32(sidx), int32(eidx), int32(tlen)}
break
} else if term.inv { } else if term.inv {
offsets = append(offsets, Offset{0, 0, 0}) offset = &Offset{0, 0, 0}
continue
}
}
if offset != nil {
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

@@ -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 {
@@ -180,6 +182,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
} }
@@ -192,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,
@@ -304,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 {
@@ -390,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() {
@@ -401,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 {
@@ -697,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
@@ -712,21 +724,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 +740,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,29 +862,33 @@ func (t *Terminal) Loop() {
} }
} }
action := t.keymap[event.Type] var doAction func(actionType, int) bool
mapkey := event.Type doAction = func(action actionType, mapkey int) bool {
if event.Type == C.Rune {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
}
switch action { switch action {
case actIgnore: case actIgnore:
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
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()
continue return false
case actToggleSort: case actToggleSort:
t.sort = !t.sort t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock() t.mutex.Unlock()
continue return false
case actBeginningOfLine: case actBeginningOfLine:
t.cx = 0 t.cx = 0
case actBackwardChar: case actBackwardChar:
@@ -1028,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
} }
@@ -1040,7 +1056,7 @@ func (t *Terminal) Loop() {
// Double-click // Double-click
if my >= min { if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose) return doAction(t.keymap[C.DoubleClick], C.DoubleClick)
} }
} }
} else if me.Down { } else if me.Down {
@@ -1057,6 +1073,19 @@ func (t *Terminal) Loop() {
} }
} }
} }
return true
}
action := t.keymap[event.Type]
mapkey := event.Type
if event.Type == C.Rune {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
}
if !doAction(action, mapkey) {
continue
}
changed := string(previousInput) != string(t.input) 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

@@ -6,9 +6,10 @@ 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 = "#{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 +214,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 +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
@@ -904,6 +943,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 +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