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

Compare commits

...

16 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
16 changed files with 293 additions and 114 deletions

View File

@@ -1,6 +1,15 @@
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 0.10.9
------ ------

View File

@@ -50,10 +50,10 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
```sh ```sh
brew reinstall --HEAD fzf brew install fzf
# Install shell extensions # Install shell extensions
/usr/local/Cellar/fzf/HEAD/install /usr/local/opt/fzf/install
``` ```
#### Install as Vim plugin #### Install as Vim plugin
@@ -78,7 +78,7 @@ while. Please follow the instruction below depending on the installation
method. method.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew reinstall --HEAD fzf` - brew: `brew update; brew reinstall fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
Usage Usage
@@ -108,32 +108,41 @@ vim $(fzf)
- Mouse: scroll, click, double-click; shift-click and shift-scroll on - Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode multi-select mode
#### Extended-search mode #### Search syntax
Since 0.10.9, fzf starts in "extended-search mode" by default. Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!rmx`
In this mode, you can specify multiple patterns delimited by spaces, | Token | Match type | Description |
such as: `^music .mp3$ sbtrkt !rmx` | -------- | -------------------- | -------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| Token | Description | Match type | | `^music` | prefix-exact-match | Items that start with `music` |
| -------- | -------------------------------- | -------------------- | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `^music` | Items that start with `music` | prefix-exact-match | | `'wild` | exact-match (quoted) | Items that include `wild` |
| `.mp3$` | Items that end with `.mp3` | suffix-exact-match | | `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` |
| `sbtrkt` | Items that match `sbtrkt` | fuzzy-match | | `!'fire` | inverse-exact-match | Items that do not include `fire` |
| `!rmx` | Items that do not match `rmx` | inverse-fuzzy-match |
| `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't prefer fuzzy matching and do not wish to "quote" every word, If you don't prefer fuzzy matching and do not wish to "quote" every word,
start fzf with `-e` or `--exact` option. Note that when `--exact` is set, start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
`'`-prefix "unquotes" the term. `'`-prefix "unquotes" the term.
A single bar character term acts as an OR operator. For example, the following
query matches entries that start with `core` and end with either `go`, `rb`,
or `py`.
```
^core go$ | rb$ | py$
```
#### Environment variables #### Environment variables
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'`
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
Examples Examples
-------- --------
@@ -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

View File

@@ -2,8 +2,8 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.10.9 pre=1 || [[ "$@" =~ --pre ]] && version=0.11.0 pre=1 ||
version=0.10.9 pre=0 version=0.11.0 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
@@ -342,7 +342,8 @@ append_line() {
echo echo
for shell in bash zsh; do for shell in bash zsh; do
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}"
done done
if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then
@@ -353,7 +354,7 @@ fi
cat << EOF cat << EOF
Finished. Restart your shell or reload config file. Finished. Restart your shell or reload config file.
source ~/.bashrc # bash source ~/.bashrc # bash
source ~/.zshrc # zsh source ${ZDOTDIR:-~}/.zshrc # zsh
EOF EOF
[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF [ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder" .TH fzf 1 "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
@@ -223,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
@@ -276,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"
@@ -394,6 +401,13 @@ If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that
when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term.
.SS OR operator
A single bar character term acts as an OR operator. For example, the following
query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
\fBrb\fR, or \fBpy\fR.
e.g. \fB^core go$ | rb$ | py$\fR
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

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

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.9" version = "0.11.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

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

@@ -466,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)
@@ -565,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)
@@ -580,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), "~!@#$%^&*;/|") {

View File

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

View File

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

View File

@@ -132,6 +132,7 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti
) )
func defaultKeymap() map[int]actionType { func defaultKeymap() map[int]actionType {
@@ -305,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
@@ -391,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() {
@@ -402,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 {
@@ -698,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
@@ -858,7 +867,17 @@ func (t *Terminal) Loop() {
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
}
case actExecuteMulti:
if len(t.selected) > 0 {
sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text)
}
executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else {
return doAction(actExecute, mapkey)
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
@@ -1023,7 +1042,7 @@ func (t *Terminal) Loop() {
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3] mx -= t.marginInt[3]
my -= t.marginInt[0] my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse { if !t.reverse {
my = t.maxHeight() - my - 1 my = t.maxHeight() - my - 1
} }

View File

@@ -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' }
@@ -915,6 +933,12 @@ class TestGoFZF < TestBase
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
end end
def test_or_operator
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines.map(&:chomp)
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
`seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp)
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -981,6 +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