diff --git a/CHANGELOG.md b/CHANGELOG.md index f46d0f0..baee404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,9 @@ CHANGELOG --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down' ``` - Added placeholder expressions - - `{fzf:action}` - the name of the last action performed - - `{fzf:query}` - synonym for `{q}` + - `{fzf:action}` - The name of the last action performed + - `{fzf:prompt}` - Prompt string (including ANSI color codes) + - `{fzf:query}` - Synonym for `{q}` - Added support for negative height ```sh # Terminal height minus 1, so you can still see the command line diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index fd68a87..9b5e1e3 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -593,6 +593,7 @@ Also, Use \fB{+n}\fR if you want all index numbers when multiple lines are selected. .br * \fB{fzf:action}\fR is replaced to to the name of the last action performed +* \fB{fzf:prompt}\fR is replaced to to the prompt string Note that you can escape a placeholder pattern by prepending a backslash. diff --git a/src/terminal.go b/src/terminal.go index ad86044..1385592 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -57,7 +57,7 @@ var actionTypeRegex *regexp.Regexp const clearCode string = "\x1b[2J" func init() { - placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action)}|{\+?f?nf?})`) + placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`) whiteSuffix = regexp.MustCompile(`\s*$`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) @@ -183,6 +183,7 @@ type Terminal struct { separator labelPrinter separatorLen int spinner []string + promptString string prompt func() promptLen int borderLabel labelPrinter @@ -670,6 +671,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { infoSep: opts.InfoSep, separator: nil, spinner: makeSpinner(opts.Unicode), + promptString: opts.Prompt, queryLen: [2]int{0, 0}, layout: opts.Layout, fullscreen: fullscreen, @@ -2354,7 +2356,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { } if strings.HasPrefix(match, "{fzf:") { - // Both {fzf:query} and {fzf:action} are not determined by the current item + // {fzf:*} are not determined by the current item flags.forceUpdate = true return false, match, flags } @@ -2421,9 +2423,30 @@ func cleanTemporaryFiles() { activeTempFiles = []string{} } +type replacePlaceholderParams struct { + template string + stripAnsi bool + delimiter Delimiter + printsep string + forcePlus bool + query string + allItems []*Item + lastAction actionType + prompt string +} + func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { - return replacePlaceholder( - template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list, t.lastAction) + return replacePlaceholder(replacePlaceholderParams{ + template: template, + stripAnsi: t.ansi, + delimiter: t.delimiter, + printsep: t.printsep, + forcePlus: forcePlus, + query: input, + allItems: list, + lastAction: t.lastAction, + prompt: t.promptString, + }) } func (t *Terminal) evaluateScrollOffset() int { @@ -2461,9 +2484,9 @@ func (t *Terminal) evaluateScrollOffset() int { return util.Max(0, base) } -func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item, lastAction actionType) string { - current := allItems[:1] - selected := allItems[1:] +func replacePlaceholder(params replacePlaceholderParams) string { + current := params.allItems[:1] + selected := params.allItems[1:] if current[0] == nil { current = []*Item{} } @@ -2472,7 +2495,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr } // replace placeholders one by one - return placeholder.ReplaceAllStringFunc(template, func(match string) string { + return placeholder.ReplaceAllStringFunc(params.template, func(match string) string { escaped, match, flags := parsePlaceholder(match) // this function implements the effects a placeholder has on items @@ -2482,17 +2505,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr switch { case escaped: return match - case match == "{fzf:action}": - name := "" - for i, r := range lastAction.String()[3:] { - if i > 0 && r >= 'A' && r <= 'Z' { - name += "-" - } - name += string(r) - } - return strings.ToLower(name) case match == "{q}" || match == "{fzf:query}": - return quoteEntry(query) + return quoteEntry(params.query) case match == "{}": replace = func(item *Item) string { switch { @@ -2503,11 +2517,22 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr } return strconv.Itoa(n) case flags.file: - return item.AsString(stripAnsi) + return item.AsString(params.stripAnsi) default: - return quoteEntry(item.AsString(stripAnsi)) + return quoteEntry(item.AsString(params.stripAnsi)) } } + case match == "{fzf:action}": + name := "" + for i, r := range params.lastAction.String()[3:] { + if i > 0 && r >= 'A' && r <= 'Z' { + name += "-" + } + name += string(r) + } + return strings.ToLower(name) + case match == "{fzf:prompt}": + return quoteEntry(params.prompt) default: // token type and also failover (below) rangeExpressions := strings.Split(match[1:len(match)-1], ",") @@ -2522,15 +2547,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr } replace = func(item *Item) string { - tokens := Tokenize(item.AsString(stripAnsi), delimiter) + tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter) trans := Transform(tokens, ranges) str := joinTokens(trans) // trim the last delimiter - if delimiter.str != nil { - str = strings.TrimSuffix(str, *delimiter.str) - } else if delimiter.regex != nil { - delims := delimiter.regex.FindAllStringIndex(str, -1) + if params.delimiter.str != nil { + str = strings.TrimSuffix(str, *params.delimiter.str) + } else if params.delimiter.regex != nil { + delims := params.delimiter.regex.FindAllStringIndex(str, -1) // make sure the delimiter is at the very end of the string if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { str = str[:delims[len(delims)-1][0]] @@ -2550,7 +2575,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr // apply 'replace' function over proper set of items and return result items := current - if flags.plus || forcePlus { + if flags.plus || params.forcePlus { items = selected } replacements := make([]string, len(items)) @@ -2560,7 +2585,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr } if flags.file { - return writeTemporaryFile(replacements, printsep) + return writeTemporaryFile(replacements, params.printsep) } return strings.Join(replacements, " ") }) @@ -3302,6 +3327,7 @@ func (t *Terminal) Loop() { } case actTransformPrompt: prompt := t.executeCommand(a.a, false, true, true, true) + t.promptString = prompt t.prompt, t.promptLen = t.parsePrompt(prompt) req(reqPrompt) case actTransformQuery: @@ -3395,6 +3421,7 @@ func (t *Terminal) Loop() { req(reqRedrawPreviewLabel) } case actChangePrompt: + t.promptString = a.a t.prompt, t.promptLen = t.parsePrompt(a.a) req(reqPrompt) case actPreview: diff --git a/src/terminal_test.go b/src/terminal_test.go index 791bebf..e7d3e75 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -12,6 +12,20 @@ import ( "github.com/junegunn/fzf/src/util" ) +func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { + return replacePlaceholder(replacePlaceholderParams{ + template: template, + stripAnsi: stripAnsi, + delimiter: delimiter, + printsep: printsep, + forcePlus: forcePlus, + query: query, + allItems: allItems, + lastAction: actBackwardDeleteCharEof, + prompt: "prompt", + }) +} + func TestReplacePlaceholder(t *testing.T) { item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") items1 := []*Item{item1, item1} @@ -52,90 +66,90 @@ func TestReplacePlaceholder(t *testing.T) { */ // {}, preserve ansi - result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") // {}, strip ansi - result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") // {}, with multiple items - result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) + result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") // {..}, strip leading whitespaces, preserve ansi - result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") // {..}, strip leading whitespaces, strip ansi - result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") // {q} - result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") // {q}, multiple items - result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore) + result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") - result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2, actIgnore) + result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") - result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) + result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") - result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2, actIgnore) + result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") // forcePlus - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2, actIgnore) + result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") // Whitespace preserving flag with "'" delimiter - result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.O}}") - result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) checkFormat("echo {{.O}}bar baz{{.O}}") - result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") - result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") // Whitespace preserving flag with regex delimiter regex = regexp.MustCompile(`\w+`) - result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) checkFormat("echo {{.O}} {{.O}}") - result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) checkFormat("echo {{.O}}{{.I}}{{.O}}") - result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) checkFormat("echo {{.O}} {{.O}}") // No match - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}, actIgnore) + result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) check("echo /") // No match, but with selections - result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}, actIgnore) + result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") // String delimiter - result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") // Regex delimiter regex = regexp.MustCompile("[oa]+") // foo'bar baz - result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1, actIgnore) + result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") /* @@ -155,7 +169,6 @@ func TestReplacePlaceholder(t *testing.T) { newItem("7a 7b 7c 7d 7e 7f"), } stripAnsi := false - printsep = "\n" forcePlus := false query := "sample query" @@ -199,7 +212,7 @@ func TestReplacePlaceholder(t *testing.T) { // while the double q is invalid, it is useful here for testing purposes templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}" - templateToOutput[`{fzf:action}`] = "backward-delete-char-eof" + templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'" // IV. escaping placeholder templateToOutput[`\{}`] = `{}` @@ -210,11 +223,11 @@ func TestReplacePlaceholder(t *testing.T) { templateToOutput[`{++}`] = templateToOutput[`{+}`] for giveTemplate, wantOutput := range templateToOutput { - result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actBackwardDeleteCharEof) + result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) checkFormat(wantOutput) } for giveTemplate, wantOutput := range templateToFile { - path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3, actIgnore) + path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) data, err := readFile(path) if err != nil { @@ -568,10 +581,10 @@ func testCommands(t *testing.T, tests []testCase) { // evaluate the test cases for idx, test := range tests { - gotOutput := replacePlaceholder( + gotOutput := replacePlaceholderTest( test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.give.query, - test.give.allItems, actIgnore) + test.give.allItems) switch { case test.want.output != "": if gotOutput != test.want.output {