diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a0a990d4..1e5ab773 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -285,6 +285,9 @@ was made) individually quoted. e.g. \fBfzf --multi --preview='head -10 {+}'\fR \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR +When using a field index expression, leading and trailing whitespace is stripped +from the replacement string. To preserve the whitespace, use the \fBs\fR flag. + Also, \fB{q}\fR is replaced to the current query string. Note that you can escape a placeholder pattern by prepending a backslash. diff --git a/src/terminal.go b/src/terminal.go index e91b3834..2daad273 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("\\\\?(?:{[+s]*[0-9,-.]*}|{q})") } type jumpMode int @@ -221,6 +221,11 @@ const ( actTop ) +type placeholderFlags struct { + plus bool + preserveSpace bool +} + func toActions(types ...actionType) []action { actions := make([]action, len(types)) for idx, t := range types { @@ -1130,12 +1135,37 @@ func quoteEntry(entry string) string { return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } +func parsePlaceholder(match string) (bool, string, placeholderFlags) { + flags := placeholderFlags{} + + if match[0] == '\\' { + // Escaped placeholder pattern + return true, match[1:], flags + } + + skipChars := 1 + for _, char := range match[1:] { + switch char { + case '+': + flags.plus = true + skipChars++ + case 's': + flags.preserveSpace = true + skipChars++ + default: + break + } + } + + matchWithoutFlags := "{" + match[skipChars:] + + return false, matchWithoutFlags, flags +} + func hasPlusFlag(template string) bool { for _, match := range placeholder.FindAllString(template, -1) { - if match[0] == '\\' { - continue - } - if match[1] == '+' { + _, _, flags := parsePlaceholder(match) + if flags.plus { return true } } @@ -1152,9 +1182,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo selected = []*Item{} } return placeholder.ReplaceAllStringFunc(template, func(match string) string { - // Escaped pattern - if match[0] == '\\' { - return match[1:] + escaped, match, flags := parsePlaceholder(match) + + if escaped { + return match } // Current query @@ -1162,13 +1193,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo return quoteEntry(query) } - plusFlag := forcePlus - if match[1] == '+' { - match = "{" + match[2:] - plusFlag = true - } items := current - if plusFlag { + if flags.plus || forcePlus { items = selected } @@ -1204,7 +1230,9 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo str = str[:delims[len(delims)-1][0]] } } - str = strings.TrimSpace(str) + if !flags.preserveSpace { + str = strings.TrimSpace(str) + } replacements[idx] = quoteEntry(str) } return strings.Join(replacements, " ") diff --git a/src/terminal_test.go b/src/terminal_test.go index 60f2b1ad..62b20c45 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -21,6 +21,9 @@ func TestReplacePlaceholder(t *testing.T) { newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} + delim := "'" + var regex *regexp.Regexp + var result string check := func(expected string) { if result != expected { @@ -72,6 +75,31 @@ func TestReplacePlaceholder(t *testing.T) { result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, 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) + check("echo ' foo'") + + result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, false, "query", items1) + check("echo 'bar baz'") + + result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, false, "query", items1) + check("echo ' foo'\\''bar baz'") + + result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, 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) + check("echo ' '") + + result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, false, "query", items1) + check("echo ''\\'''") + + result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, false, "query", items1) + check("echo ' '") + // No match result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) check("echo /") @@ -81,12 +109,11 @@ func TestReplacePlaceholder(t *testing.T) { check("echo /' foo'\\''bar baz'") // String delimiter - delim := "'" result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1) check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") // Regex delimiter - regex := regexp.MustCompile("[oa]+") + regex = regexp.MustCompile("[oa]+") // foo'bar baz result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")