summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2017-01-27 16:38:42 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2017-01-27 16:38:42 +0900
commited57dcb924112192636260fb31d0db54352c517a (patch)
treec7a6e58cc6e92d32caa004a078c57eab8f7c09af
parent95c77bfb98eff07c63599bb02a58f73d6c143e62 (diff)
Extend placeholder expression for multiple selections
Close #788
-rw-r--r--CHANGELOG.md8
-rw-r--r--man/man1/fzf.114
-rw-r--r--src/terminal.go109
-rw-r--r--test/test_go.rb52
4 files changed, 141 insertions, 42 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4358226..4dd6e9c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
CHANGELOG
=========
+0.16.3
+------
+- Fixed a bug where fzf incorrectly display the lines when straddling tab
+ characters are trimmed
+- Placeholder expression used in `--preview` and `execute` action can
+ optionally take `+` flag to be used with multiple selections
+ - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
+
0.16.2
------
- Dropped ncurses dependency
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 9a374d5b..e4321baa 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -262,13 +262,21 @@ Execute the given command for the current line and display the result on the
preview window. \fB{}\fR in the command is the placeholder that is replaced to
the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX
-EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current
-query string.
+EXPRESSION\fR for the details).
.RS
e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
+A placeholder expression starting with \fB+\fR flag will be replaced to the
+space-separated list of the selected lines (or the current line if no selection
+was made) individually quoted.
+
+e.g. \fBfzf --multi --preview="head -10 {+}"\fR
+ \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
+
+Also, \fB{q}\fR is replaced to the current query string.
+
Note that you can escape a placeholder pattern by prepending a backslash.
.RE
.TP
@@ -461,7 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
- \fBexecute-multi(...)\fR (see below for the details)
+ \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
diff --git a/src/terminal.go b/src/terminal.go
index 081f7156..43d21d88 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -24,7 +24,7 @@ import (
var placeholder *regexp.Regexp
func init() {
- placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
+ placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
}
type jumpMode int
@@ -436,9 +436,9 @@ func (t *Terminal) output() bool {
}
found := len(t.selected) > 0
if !found {
- cnt := t.merger.Length()
- if cnt > 0 && cnt > t.cy {
- t.printer(t.current())
+ current := t.currentItem()
+ if current != nil {
+ t.printer(current.AsString(t.ansi))
found = true
}
} else {
@@ -1044,7 +1044,27 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
-func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
+func hasPlusFlag(template string) bool {
+ for _, match := range placeholder.FindAllString(template, -1) {
+ if match[0] == '\\' {
+ continue
+ }
+ if match[1] == '+' {
+ return true
+ }
+ }
+ return false
+}
+
+func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
+ current := allItems[:1]
+ selected := allItems[1:]
+ if current[0] == nil {
+ current = []*Item{}
+ }
+ if selected[0] == nil {
+ selected = []*Item{}
+ }
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern
if match[0] == '\\' {
@@ -1056,6 +1076,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
return quoteEntry(query)
}
+ plusFlag := forcePlus
+ if match[1] == '+' {
+ match = "{" + match[2:]
+ plusFlag = true
+ }
+ items := current
+ if plusFlag {
+ items = selected
+ }
+
replacements := make([]string, len(items))
if match == "{}" {
@@ -1096,8 +1126,12 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
})
}
-func (t *Terminal) executeCommand(template string, items []*Item) {
- command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items)
+func (t *Terminal) executeCommand(template string, forcePlus bool) {
+ valid, list := t.buildPlusList(template, forcePlus)
+ if !valid {
+ return
+ }
+ command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@@ -1123,11 +1157,24 @@ func (t *Terminal) hasPreviewWindow() bool {
}
func (t *Terminal) currentItem() *Item {
- return t.merger.Get(t.cy).item
+ cnt := t.merger.Length()
+ if cnt > 0 && cnt > t.cy {
+ return t.merger.Get(t.cy).item
+ }
+ return nil
}
-func (t *Terminal) current() string {
- return t.currentItem().AsString(t.ansi)
+func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
+ current := t.currentItem()
+ if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
+ return current != nil, []*Item{current, current}
+ }
+ sels := make([]*Item, len(t.selected)+1)
+ sels[0] = current
+ for i, sel := range t.sortSelected() {
+ sels[i+1] = sel.item
+ }
+ return true, sels
}
// Loop is called to start Terminal I/O
@@ -1184,19 +1231,20 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() {
go func() {
for {
- var request *Item
+ var request []*Item
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
- request = value.(*Item)
+ request = value.([]*Item)
}
}
events.Clear()
})
- if request != nil {
+ // We don't display preview window if no match
+ if request[0] != nil {
command := replacePlaceholder(t.preview.command,
- t.ansi, t.delimiter, string(t.input), []*Item{request})
+ t.ansi, t.delimiter, false, string(t.input), request)
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -1232,17 +1280,12 @@ func (t *Terminal) Loop() {
t.printInfo()
case reqList:
t.printList()
- cnt := t.merger.Length()
- var currentFocus *Item
- if cnt > 0 && cnt > t.cy {
- currentFocus = t.currentItem()
- } else {
- currentFocus = nil
- }
+ currentFocus := t.currentItem()
if currentFocus != focused {
focused = currentFocus
if t.isPreviewEnabled() {
- t.previewBox.Set(reqPreviewEnqueue, focused)
+ _, list := t.buildPlusList(t.preview.command, false)
+ t.previewBox.Set(reqPreviewEnqueue, list)
}
}
case reqJump:
@@ -1348,19 +1391,9 @@ func (t *Terminal) Loop() {
switch a.t {
case actIgnore:
case actExecute:
- if t.cy >= 0 && t.cy < t.merger.Length() {
- t.executeCommand(a.a, []*Item{t.currentItem()})
- }
+ t.executeCommand(a.a, false)
case actExecuteMulti:
- if len(t.selected) > 0 {
- sels := make([]*Item, len(t.selected))
- for i, sel := range t.sortSelected() {
- sels[i] = sel.item
- }
- t.executeCommand(a.a, sels)
- } else {
- return doAction(action{t: actExecute, a: a.a}, mapkey)
- }
+ t.executeCommand(a.a, true)
case actInvalid:
t.mutex.Unlock()
return false
@@ -1369,9 +1402,11 @@ func (t *Terminal) Loop() {
t.previewer.enabled = !t.previewer.enabled
t.tui.Clear()
t.resizeWindows()
- cnt := t.merger.Length()
- if t.previewer.enabled && cnt > 0 && cnt > t.cy {
- t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
+ if t.previewer.enabled {
+ valid, list := t.buildPlusList(t.preview.command, false)
+ if valid {
+ t.previewBox.Set(reqPreviewEnqueue, list)
+ }
}
req(reqList, reqInfo, reqHeader)
}
diff --git a/test/test_go.rb b/test/test_go.rb
index cdd96d10..f730f256 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -879,7 +879,7 @@ class TestGoFZF < TestBase
def test_execute_multi
output = '/tmp/fzf-test-execute-multi'
- opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
+ opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' }
@@ -902,6 +902,43 @@ class TestGoFZF < TestBase
File.unlink output rescue nil
end
+ def test_execute_plus_flag
+ output = tempname + ".tmp"
+ File.unlink output rescue nil
+ writelines tempname, ["foo bar", "123 456"]
+
+ tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
+
+ execute = lambda do
+ tmux.send_keys 'x', 'y'
+ tmux.until { |lines| lines[-2].include? '0/2' }
+ tmux.send_keys :BSpace
+ tmux.until { |lines| lines[-2].include? '2/2' }
+ end
+
+ tmux.until { |lines| lines[-2].include? '2/2' }
+ execute.call
+
+ tmux.send_keys :Up
+ tmux.send_keys :Tab
+ execute.call
+
+ tmux.send_keys :Tab
+ execute.call
+
+ tmux.send_keys :Enter
+ tmux.prepare
+ readonce
+
+ assert_equal [
+ %[foo bar/foo bar/bar/bar],
+ %[123 456/foo bar/456/bar],
+ %[123 456 foo bar/foo bar/456 bar/bar]
+ ], File.readlines(output).map(&:chomp)
+ rescue
+ File.unlink output rescue nil
+ end
+
def test_execute_shell
# Custom script to use as $SHELL
output = tempname + '.out'
@@ -1198,7 +1235,7 @@ class TestGoFZF < TestBase
end
def test_preview
- tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter
+ tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') }
@@ -1212,6 +1249,17 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
tmux.send_keys 'foobar'
tmux.until { |lines| !lines[1].include?('{') }
+ tmux.send_keys 'C-u'
+ tmux.until { |lines| lines.match_count == 1000 }
+ tmux.until { |lines| lines[1].include?(' {1-1}') }
+ tmux.send_keys :BTab
+ tmux.until { |lines| lines[1].include?(' {-1}') }
+ tmux.send_keys :BTab
+ tmux.until { |lines| lines[1].include?(' {3-1 }') }
+ tmux.send_keys :BTab
+ tmux.until { |lines| lines[1].include?(' {4-1 3}') }
+ tmux.send_keys :BTab
+ tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
end
def test_preview_hidden