summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-12-28 17:10:06 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-12-28 17:10:06 +0900
commit5d360180afdb059a30ac7912edf9bc94ea8d2fe3 (patch)
treed75b0b0cecfb01430453b6a63b94bc4d6c56872b
parentf0fbed6007d628216a5b75b62646aea90d489afa (diff)
Add {fzf:prompt} placeholder expression
Close #3354
-rw-r--r--CHANGELOG.md5
-rw-r--r--man/man1/fzf.11
-rw-r--r--src/terminal.go81
-rw-r--r--src/terminal_test.go71
4 files changed, 100 insertions, 58 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f46d0f08..baee4046 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 fd68a874..9b5e1e3a 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 ad860441..13855925 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 791bebf4..e7d3e751 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 {