summaryrefslogtreecommitdiff
path: root/src/terminal.go
diff options
context:
space:
mode:
authorVlastimil Ovčáčík <vovcacik@github.ovcacik.org>2021-10-15 15:31:59 +0200
committerGitHub <noreply@github.com>2021-10-15 22:31:59 +0900
commit61339a8ae2f6be27f28c243f00a41cc3aa5f54c2 (patch)
treee8bab761dde12006078deedffe1555ab66cce095 /src/terminal.go
parent50eb2e38552f57bce84c417fad8f4b48fcbf16ac (diff)
downloadfzf-61339a8ae2f6be27f28c243f00a41cc3aa5f54c2.tar.gz
Add more tests of placeholder flags and simplify its logic (#2624)
* [tests] Test fzf's placeholders and escaping on practical commands This tests some reasonable commands in fzf's templates (for commands, previews, rebinds etc.), how are those commands escaped (backslashes, double quotes), and documents if the output is executable in cmd.exe. Both on Unix and Windows. * [tests] Add testing of placeholder parsing and matching Adds tests and bit of docs for the curly brackets placeholders in fzf's template strings. Also tests the "placeholder" regex. * [tests] Add more test cases of replacing placeholders focused on flags Replacing placeholders in templates is already tested, this adds tests that focus more on the parameters of placeholders - e.g. flags, token ranges. There is at least one test for each flag, not all combinations are tested though. * [refactoring] Split OS-specific function quoteEntry() to corresponding source file This is minor refactoring, and also the function's test was made crossplatform. * [refactoring] Simplify replacePlaceholder function Should be equivalent to the original, but has simpler structure.
Diffstat (limited to 'src/terminal.go')
-rw-r--r--src/terminal.go154
1 files changed, 84 insertions, 70 deletions
diff --git a/src/terminal.go b/src/terminal.go
index c296e443..8a8e1658 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -23,6 +23,26 @@ import (
// import "github.com/pkg/profile"
+/*
+ Placeholder regex is used to extract placeholders from fzf's template
+ strings. Acts as input validation for parsePlaceholder function.
+ Describes the syntax, but it is fairly lenient.
+
+ The following pseudo regex has been reverse engineered from the
+ implementation. It is overly strict, but better describes whats possible.
+ As such it is not useful for validation, but rather to generate test
+ cases for example.
+
+ \\?(?: # escaped type
+ {\+?s?f?RANGE(?:,RANGE)*} # token type
+ |{q} # query type
+ |{\+?n?f?} # item type (notice no mandatory element inside brackets)
+ )
+ RANGE = (?:
+ (?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y)
+ |-?[0-9]+ # shorthand syntax (x..x)
+ )
+*/
var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
@@ -1520,22 +1540,6 @@ func keyMatch(key tui.Event, event tui.Event) bool {
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
}
-func quoteEntryCmd(entry string) string {
- escaped := strings.Replace(entry, `\`, `\\`, -1)
- escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
- r, _ := regexp.Compile(`[&|<>()@^%!"]`)
- return r.ReplaceAllStringFunc(escaped, func(match string) string {
- return "^" + match
- })
-}
-
-func quoteEntry(entry string) string {
- if util.IsWindows() {
- return quoteEntryCmd(entry)
- }
- return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
-}
-
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags := placeholderFlags{}
@@ -1561,6 +1565,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
skipChars++
case 'q':
flags.query = true
+ // query flag is not skipped
default:
break
}
@@ -1648,77 +1653,86 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
if selected[0] == nil {
selected = []*Item{}
}
+
+ // replace placeholders one by one
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
escaped, match, flags := parsePlaceholder(match)
- if escaped {
- return match
- }
+ // this function implements the effects a placeholder has on items
+ var replace func(*Item) string
- // Current query
- if match == "{q}" {
+ // placeholder types (escaped, query type, item type, token type)
+ switch {
+ case escaped:
+ return match
+ case match == "{q}":
return quoteEntry(query)
- }
-
- items := current
- if flags.plus || forcePlus {
- items = selected
- }
-
- replacements := make([]string, len(items))
-
- if match == "{}" {
- for idx, item := range items {
- if flags.number {
+ case match == "{}":
+ replace = func(item *Item) string {
+ switch {
+ case flags.number:
n := int(item.text.Index)
if n < 0 {
- replacements[idx] = ""
- } else {
- replacements[idx] = strconv.Itoa(n)
+ return ""
}
- } else if flags.file {
- replacements[idx] = item.AsString(stripAnsi)
- } else {
- replacements[idx] = quoteEntry(item.AsString(stripAnsi))
+ return strconv.Itoa(n)
+ case flags.file:
+ return item.AsString(stripAnsi)
+ default:
+ return quoteEntry(item.AsString(stripAnsi))
}
}
- if flags.file {
- return writeTemporaryFile(replacements, printsep)
+ default:
+ // token type and also failover (below)
+ rangeExpressions := strings.Split(match[1:len(match)-1], ",")
+ ranges := make([]Range, len(rangeExpressions))
+ for idx, s := range rangeExpressions {
+ r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
+ if !ok {
+ // Invalid expression, just return the original string in the template
+ return match
+ }
+ ranges[idx] = r
}
- return strings.Join(replacements, " ")
- }
- tokens := strings.Split(match[1:len(match)-1], ",")
- ranges := make([]Range, len(tokens))
- for idx, s := range tokens {
- r, ok := ParseRange(&s)
- if !ok {
- // Invalid expression, just return the original string in the template
- return match
+ replace = func(item *Item) string {
+ tokens := Tokenize(item.AsString(stripAnsi), 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)
+ // 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]]
+ }
+ }
+
+ if !flags.preserveSpace {
+ str = strings.TrimSpace(str)
+ }
+ if !flags.file {
+ str = quoteEntry(str)
+ }
+ return str
}
- ranges[idx] = r
}
+ // apply 'replace' function over proper set of items and return result
+
+ items := current
+ if flags.plus || forcePlus {
+ items = selected
+ }
+ replacements := make([]string, len(items))
+
for idx, item := range items {
- tokens := Tokenize(item.AsString(stripAnsi), delimiter)
- trans := Transform(tokens, ranges)
- str := joinTokens(trans)
- if delimiter.str != nil {
- str = strings.TrimSuffix(str, *delimiter.str)
- } else if delimiter.regex != nil {
- delims := delimiter.regex.FindAllStringIndex(str, -1)
- if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
- str = str[:delims[len(delims)-1][0]]
- }
- }
- if !flags.preserveSpace {
- str = strings.TrimSpace(str)
- }
- if !flags.file {
- str = quoteEntry(str)
- }
- replacements[idx] = str
+ replacements[idx] = replace(item)
}
+
if flags.file {
return writeTemporaryFile(replacements, printsep)
}