From a89d8995c3a4544851ae3a40b8fb1f1c16f9535e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 01:42:01 +0900 Subject: [PATCH] Add execute-multi action Close #413 --- man/man1/fzf.1 | 7 +++++++ src/options.go | 32 ++++++++++++++++++++++++-------- src/terminal.go | 37 ++++++++++++++++++++++++++++--------- test/test_go.rb | 18 ++++++++++++++++++ 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 200464a..50de48e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -223,6 +223,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) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\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 comma-separated list. .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 .TP .BI "--history=" "HISTORY_FILE" diff --git a/src/options.go b/src/options.go index 42b27f3..d1f8201 100644 --- a/src/options.go +++ b/src/options.go @@ -466,10 +466,13 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } 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{escapedComma, ':'}), -1) @@ -565,11 +568,18 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b toggleSort = true default: if isExecuteAction(actLower) { - keymap[key] = actExecute - if act[7] == ':' { - execmap[key] = act[8:] + var offset int + if strings.HasPrefix(actLower, "execute-multi") { + keymap[key] = actExecuteMulti + offset = len("execute-multi") } 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 { 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 { - if !strings.HasPrefix(str, "execute") || len(str) < 9 { + if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { 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] if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { diff --git a/src/terminal.go b/src/terminal.go index eb82bd8..764459f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -132,6 +132,7 @@ const ( actPreviousHistory actNextHistory actExecute + actExecuteMulti ) func defaultKeymap() map[int]actionType { @@ -305,18 +306,22 @@ func (t *Terminal) output() bool { found = true } } else { - sels := make([]selectedItem, 0, len(t.selected)) - for _, sel := range t.selected { - sels = append(sels, sel) - } - sort.Sort(byTimeOrder(sels)) - for _, sel := range sels { + for _, sel := range t.sortSelected() { fmt.Println(*sel.text) } } 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 { if r == '\t' { return 8 - prefixWidth%8 @@ -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 } -func executeCommand(template string, current string) { - command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1) +func quoteEntry(entry string) string { + return fmt.Sprintf("%q", entry) +} + +func executeCommand(template string, replacement string) { + command := strings.Replace(template, "{}", replacement, -1) cmd := exec.Command("sh", "-c", command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -858,7 +867,17 @@ func (t *Terminal) Loop() { case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) + executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) + } + case actExecuteMulti: + if len(t.selected) > 0 { + sels := make([]string, len(t.selected)) + for i, sel := range t.sortSelected() { + sels[i] = quoteEntry(*sel.text) + } + executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) + } else { + return doAction(actExecute, mapkey) } case actInvalid: t.mutex.Unlock() diff --git a/test/test_go.rb b/test/test_go.rb index 7143d36..e3b5274 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -713,6 +713,24 @@ class TestGoFZF < TestBase File.unlink output rescue nil 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 tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.until { |lines| lines[-2].include? '8/8' }