mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-08 16:12:09 -07:00
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.
This commit is contained in:
committed by
GitHub
parent
50eb2e3855
commit
61339a8ae2
158
src/terminal.go
158
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 {
|
||||
// this function implements the effects a placeholder has on items
|
||||
var replace func(*Item) string
|
||||
|
||||
// placeholder types (escaped, query type, item type, token type)
|
||||
switch {
|
||||
case escaped:
|
||||
return match
|
||||
case match == "{q}":
|
||||
return quoteEntry(query)
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
case flags.number:
|
||||
n := int(item.text.Index)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(n)
|
||||
case flags.file:
|
||||
return item.AsString(stripAnsi)
|
||||
default:
|
||||
return quoteEntry(item.AsString(stripAnsi))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Current query
|
||||
if match == "{q}" {
|
||||
return quoteEntry(query)
|
||||
}
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
items = selected
|
||||
}
|
||||
|
||||
replacements := make([]string, len(items))
|
||||
|
||||
if match == "{}" {
|
||||
for idx, item := range items {
|
||||
if flags.number {
|
||||
n := int(item.text.Index)
|
||||
if n < 0 {
|
||||
replacements[idx] = ""
|
||||
} 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, " ")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
ranges[idx] = r
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user