m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-17 15:53:39 -05:00

Add {*} placeholder flag

This commit is contained in:
Junegunn Choi
2025-06-19 22:35:23 +09:00
parent 16d338da84
commit dcec6354f5
5 changed files with 115 additions and 78 deletions

View File

@@ -66,7 +66,7 @@ const maxFocusEvents = 10000
const blockDuration = 1 * time.Second
func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
placeholder = regexp.MustCompile(`\\?(?:{[+*sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@@ -692,6 +692,7 @@ func processExecution(action actionType) bool {
type placeholderFlags struct {
plus bool
asterisk bool
preserveSpace bool
number bool
forceUpdate bool
@@ -713,7 +714,7 @@ type searchRequest struct {
type previewRequest struct {
template string
scrollOffset int
list []*Item
list [3][]*Item // current, select, and all matched items
env []string
query string
}
@@ -4099,6 +4100,8 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
trimmed := ""
for _, char := range match[1:] {
switch char {
case '*':
flags.asterisk = true
case '+':
flags.plus = true
case 's':
@@ -4122,19 +4125,16 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return false, matchWithoutFlags, flags
}
func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
func hasPreviewFlags(template string) (slot bool, plus bool, asterisk bool, forceUpdate bool) {
for _, match := range placeholder.FindAllString(template, -1) {
escaped, _, flags := parsePlaceholder(match)
if escaped {
continue
}
if flags.plus {
plus = true
}
if flags.forceUpdate {
forceUpdate = true
}
slot = true
plus = plus || flags.plus
asterisk = asterisk || flags.asterisk
forceUpdate = forceUpdate || flags.forceUpdate
}
return
}
@@ -4146,17 +4146,17 @@ type replacePlaceholderParams struct {
printsep string
forcePlus bool
query string
allItems []*Item
allItems [3][]*Item // current, select, and all matched items
lastAction actionType
prompt string
executor *util.Executor
}
func (t *Terminal) replacePlaceholderInInitialCommand(template string) (string, []string) {
return t.replacePlaceholder(template, false, string(t.input), []*Item{nil, nil})
return t.replacePlaceholder(template, false, string(t.input), [3][]*Item{nil, nil, nil})
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) (string, []string) {
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list [3][]*Item) (string, []string) {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: t.ansi,
@@ -4177,7 +4177,11 @@ func (t *Terminal) evaluateScrollOffset() int {
}
// We only need the current item to calculate the scroll offset
replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", []*Item{t.currentItem(), nil})
current := []*Item{t.currentItem()}
if current[0] == nil {
current = nil
}
replaced, tempFiles := t.replacePlaceholder(t.activePreviewOpts.scroll, false, "", [3][]*Item{current, nil, nil})
removeFiles(tempFiles)
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "")
@@ -4209,14 +4213,9 @@ func (t *Terminal) evaluateScrollOffset() int {
func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
tempFiles := []string{}
current := params.allItems[:1]
selected := params.allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
current := params.allItems[0]
selected := params.allItems[1]
matched := params.allItems[2]
// replace placeholders one by one
replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
@@ -4312,7 +4311,9 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || params.forcePlus {
if flags.asterisk {
items = matched
} else if flags.plus || params.forcePlus {
items = selected
}
replacements := make([]string, len(items))
@@ -4546,11 +4547,15 @@ func (t *Terminal) currentItem() *Item {
return nil
}
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) {
current := t.currentItem()
slot, plus, forceUpdate := hasPreviewFlags(template)
if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current}
slot, plus, asterisk, forceUpdate := hasPreviewFlags(template)
if !(!slot || forceUpdate || asterisk || (forcePlus || plus) && len(t.selected) > 0) {
if current == nil {
// Invalid
return false, [3][]*Item{nil, nil, nil}
}
return true, [3][]*Item{{current}, {current}, nil}
}
// We would still want to update preview window even if there is no match if
@@ -4561,17 +4566,25 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item
current = &minItem
}
var sels []*Item
if len(t.selected) == 0 {
sels = []*Item{current, current}
} else {
sels = make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
var all []*Item
if asterisk {
cnt := t.merger.Length()
all = make([]*Item, cnt)
for i := 0; i < cnt; i++ {
all[i] = t.merger.Get(i).item
}
}
return true, sels
var sels []*Item
if len(t.selected) == 0 {
sels = []*Item{current}
} else if len(t.selected) > 0 {
sels = make([]*Item, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = sel.item
}
}
return true, [3][]*Item{{current}, sels, all}
}
func (t *Terminal) selectItem(item *Item) bool {
@@ -4831,7 +4844,8 @@ func (t *Terminal) Loop() error {
stop := false
t.previewBox.WaitFor(reqPreviewReady)
for {
var items []*Item
requested := false
var items [3][]*Item
var commandTemplate string
var env []string
var query string
@@ -4849,6 +4863,7 @@ func (t *Terminal) Loop() error {
items = request.list
env = request.env
query = request.query
requested = true
}
}
events.Clear()
@@ -4856,7 +4871,7 @@ func (t *Terminal) Loop() error {
if stop {
break
}
if items == nil {
if !requested {
continue
}
version++
@@ -6396,7 +6411,7 @@ func (t *Terminal) Loop() error {
// We run the command even when there's no match
// 1. If the template doesn't have any slots
// 2. If the template has {q}
slot, _, forceUpdate := hasPreviewFlags(a.a)
slot, _, _, forceUpdate := hasPreviewFlags(a.a)
valid = !slot || forceUpdate
}
if valid {
@@ -6585,7 +6600,7 @@ func (t *Terminal) Loop() error {
}
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
_, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
_, _, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
if forceUpdate {
t.version++
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/junegunn/fzf/src/util"
)
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
@@ -30,11 +30,11 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
items1 := [3][]*Item{{item1}, {item1}, nil}
items2 := [3][]*Item{
{newItem("foo'bar \x1b[31mbaz\x1b[m")},
{newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
delim := "'"
var regex *regexp.Regexp
@@ -145,11 +145,11 @@ func TestReplacePlaceholder(t *testing.T) {
checkFormat("echo {{.O}} {{.O}}")
// No match
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter
@@ -166,17 +166,18 @@ func TestReplacePlaceholder(t *testing.T) {
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
see: TestParsePlaceholder
*/
items3 := []*Item{
items3 := [3][]*Item{
// single line
newItem("1a 1b 1c 1d 1e 1f"),
{newItem("1a 1b 1c 1d 1e 1f")},
// multi line
newItem("1a 1b 1c 1d 1e 1f"),
newItem("2a 2b 2c 2d 2e 2f"),
newItem("3a 3b 3c 3d 3e 3f"),
newItem("4a 4b 4c 4d 4e 4f"),
newItem("5a 5b 5c 5d 5e 5f"),
newItem("6a 6b 6c 6d 6e 6f"),
newItem("7a 7b 7c 7d 7e 7f"),
{newItem("1a 1b 1c 1d 1e 1f"),
newItem("2a 2b 2c 2d 2e 2f"),
newItem("3a 3b 3c 3d 3e 3f"),
newItem("4a 4b 4c 4d 4e 4f"),
newItem("5a 5b 5c 5d 5e 5f"),
newItem("6a 6b 6c 6d 6e 6f"),
newItem("7a 7b 7c 7d 7e 7f")},
nil,
}
stripAnsi := false
forcePlus := false
@@ -557,14 +558,14 @@ func newItem(str string) *Item {
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
// Functions tested in this file require array of items (allItems). The array needs
// to consist of at least two nils. This is helper function.
func newItems(str ...string) []*Item {
result := make([]*Item, util.Max(len(str), 2))
// Functions tested in this file require array of items (allItems).
// This is helper function.
func newItems(str ...string) [3][]*Item {
result := make([]*Item, len(str))
for i, s := range str {
result[i] = newItem(s)
}
return result
return [3][]*Item{result, nil, nil}
}
// (for logging purposes)
@@ -588,7 +589,7 @@ func templateToString(format string, data any) string {
type give struct {
template string
query string
allItems []*Item
allItems [3][]*Item
}
type want struct {
/*
@@ -626,25 +627,25 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases
for idx, test := range tests {
gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query,
test.give.allItems)
test.template, stripAnsi, delimiter, printsep, forcePlus,
test.query,
test.allItems)
switch {
case test.want.output != "":
if gotOutput != test.want.output {
case test.output != "":
if gotOutput != test.output {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.output)
test.template, test.query, test.allItems,
gotOutput, test.output)
}
case test.want.match != "":
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
case test.match != "":
wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
wantRegex := regexp.MustCompile(wantMatch)
if !wantRegex.MatchString(gotOutput) {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx,
test.give.template, test.give.query, test.give.allItems,
gotOutput, test.want.match)
test.template, test.query, test.allItems,
gotOutput, test.match)
}
default:
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)