From 391669a451c4eedd74dd6e1794601d2ca793340c Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Sun, 27 Oct 2019 14:50:12 +0000 Subject: [PATCH] Add 'f' flag for placeholder expression (#1733) If present the contents of the selection will be placed in a temporary file, and the filename will be placed into the string instead. --- src/options.go | 4 ++++ src/terminal.go | 52 +++++++++++++++++++++++++++++++++++++++----- src/terminal_test.go | 48 ++++++++++++++++++++-------------------- test/test_go.rb | 9 ++++++++ 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/options.go b/src/options.go index 3449f3d..bd2a038 100644 --- a/src/options.go +++ b/src/options.go @@ -189,6 +189,7 @@ type Options struct { PrintQuery bool ReadZero bool Printer func(string) + PrintSep string Sync bool History *History Header []string @@ -240,6 +241,7 @@ func defaultOptions() *Options { PrintQuery: false, ReadZero: false, Printer: func(str string) { fmt.Println(str) }, + PrintSep: "\n", Sync: false, History: nil, Header: make([]string, 0), @@ -1106,8 +1108,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.ReadZero = false case "--print0": opts.Printer = func(str string) { fmt.Print(str, "\x00") } + opts.PrintSep = "\x00" case "--no-print0": opts.Printer = func(str string) { fmt.Println(str) } + opts.PrintSep = "\n" case "--print-query": opts.PrintQuery = true case "--no-print-query": diff --git a/src/terminal.go b/src/terminal.go index 1b44e14..bb29d1d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "os" "os/signal" "regexp" @@ -22,9 +23,11 @@ import ( // import "github.com/pkg/profile" var placeholder *regexp.Regexp +var activeTempFiles []string func init() { - placeholder = regexp.MustCompile("\\\\?(?:{[+s]*[0-9,-.]*}|{q}|{\\+?n})") + placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})") + activeTempFiles = []string{} } type jumpMode int @@ -103,6 +106,7 @@ type Terminal struct { jumping jumpMode jumpLabels string printer func(string) + printsep string merger *Merger selected map[int32]selectedItem version int64 @@ -231,6 +235,7 @@ type placeholderFlags struct { preserveSpace bool number bool query bool + file bool } func toActions(types ...actionType) []action { @@ -407,6 +412,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, + printsep: opts.PrintSep, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), @@ -1207,6 +1213,9 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { case 'n': flags.number = true skipChars++ + case 'f': + flags.file = true + skipChars++ case 'q': flags.query = true default: @@ -1232,7 +1241,27 @@ func hasPreviewFlags(template string) (plus bool, query bool) { return } -func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string { +func writeTemporaryFile(data []string, printSep string) string { + f, err := ioutil.TempFile("", "fzf-preview-*") + if err != nil { + errorExit("Unable to create temporary file") + } + defer f.Close() + + f.WriteString(strings.Join(data, printSep)) + f.WriteString(printSep) + activeTempFiles = append(activeTempFiles, f.Name()) + return f.Name() +} + +func cleanTemporaryFiles() { + for _, filename := range activeTempFiles { + os.Remove(filename) + } + activeTempFiles = []string{} +} + +func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { current := allItems[:1] selected := allItems[1:] if current[0] == nil { @@ -1269,10 +1298,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo } else { replacements[idx] = strconv.Itoa(n) } + } else if flags.file { + replacements[idx] = item.AsString(stripAnsi) } else { replacements[idx] = quoteEntry(item.AsString(stripAnsi)) } } + if flags.file { + return writeTemporaryFile(replacements, printsep) + } return strings.Join(replacements, " ") } @@ -1302,7 +1336,13 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo if !flags.preserveSpace { str = strings.TrimSpace(str) } - replacements[idx] = quoteEntry(str) + if !flags.file { + str = quoteEntry(str) + } + replacements[idx] = str + } + if flags.file { + return writeTemporaryFile(replacements, printsep) } return strings.Join(replacements, " ") }) @@ -1319,7 +1359,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo if !valid { return } - command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) + command := replacePlaceholder(template, t.ansi, t.delimiter, t.printsep, forcePlus, string(t.input), list) cmd := util.ExecCommand(command, false) if !background { cmd.Stdin = os.Stdin @@ -1335,6 +1375,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo cmd.Run() t.tui.Resume(false) } + cleanTemporaryFiles() } func (t *Terminal) hasPreviewer() bool { @@ -1492,7 +1533,7 @@ func (t *Terminal) Loop() { // We don't display preview window if no match if request[0] != nil { command := replacePlaceholder(t.preview.command, - t.ansi, t.delimiter, false, string(t.input), request) + t.ansi, t.delimiter, t.printsep, false, string(t.input), request) cmd := util.ExecCommand(command, true) if t.pwindow != nil { env := os.Environ() @@ -1534,6 +1575,7 @@ func (t *Terminal) Loop() { if out.Len() > 0 || !<-updateChan { t.reqBox.Set(reqPreviewDisplay, out.String()) } + cleanTemporaryFiles() } else { t.reqBox.Set(reqPreviewDisplay, "") } diff --git a/src/terminal_test.go b/src/terminal_test.go index 62b20c4..8b828af 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -30,92 +30,92 @@ func TestReplacePlaceholder(t *testing.T) { t.Errorf("expected: %s, actual: %s", expected, result) } } - + printsep := "\n" // {}, preserve ansi - result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") // {}, strip ansi - result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") // {}, with multiple items - result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2) + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar baz'") // {..}, strip leading whitespaces, preserve ansi - result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") // {..}, strip leading whitespaces, strip ansi - result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar baz'") // {q} - result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz' 'query'") // {q}, multiple items - result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2) + result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") - result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2) + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") - result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1) + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") - result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2) + result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") // forcePlus - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") // Whitespace preserving flag with "'" delimiter - result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, false, "query", items1) + result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'") - result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, false, "query", items1) + result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo 'bar baz'") - result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, false, "query", items1) + result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") - result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, false, "query", items1) + result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'") // Whitespace preserving flag with regex delimiter regex = regexp.MustCompile("\\w+") - result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, false, "query", items1) + result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' '") - result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, false, "query", items1) + result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ''\\'''") - result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, false, "query", items1) + result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' '") // No match - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) check("echo /") // No match, but with selections - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1}) + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) check("echo /' foo'\\''bar baz'") // String delimiter - result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1) + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") // Regex delimiter regex = regexp.MustCompile("[oa]+") // foo'bar baz - result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") } diff --git a/test/test_go.rb b/test/test_go.rb index 4d3fa64..724d847 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1417,6 +1417,15 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[1].include?('{//1 10/1 10 /123//0 9}') } end + def test_preview_file + tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter + tmux.until { |lines| lines[1].include?('foo barbar00') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?('foo barbar00') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?('foo barbar foobarfoo0101') } + end + def test_preview_q_no_match tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter tmux.until { |lines| lines.match_count == 0 }