mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 06:43:47 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df468fc482 | ||
|
|
31278bcc68 | ||
|
|
e7e86b68f4 | ||
|
|
a89d8995c3 | ||
|
|
dbc854d5f4 | ||
|
|
f1cd0e2daf | ||
|
|
90d32bd756 | ||
|
|
e99731ea85 | ||
|
|
15659ac6e6 | ||
|
|
3ef41845a9 | ||
|
|
c84e681581 | ||
|
|
c3cf3427b1 | ||
|
|
2c4f71d85b | ||
|
|
c6328affae | ||
|
|
aaef18295d | ||
|
|
14f0d2035e |
@@ -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
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -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
|
||||||
|
|||||||
9
install
9
install
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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__() (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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), "~!@#$%^&*;/|") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user