Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
This commit is contained in:
parent
04492bab10
commit
3066b206af
109
src/terminal.go
109
src/terminal.go
@ -20,6 +20,12 @@ import (
|
|||||||
|
|
||||||
// import "github.com/pkg/profile"
|
// import "github.com/pkg/profile"
|
||||||
|
|
||||||
|
var placeholder *regexp.Regexp
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
|
||||||
|
}
|
||||||
|
|
||||||
type jumpMode int
|
type jumpMode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -51,6 +57,7 @@ type Terminal struct {
|
|||||||
multi bool
|
multi bool
|
||||||
sort bool
|
sort bool
|
||||||
toggleSort bool
|
toggleSort bool
|
||||||
|
delimiter Delimiter
|
||||||
expect map[int]string
|
expect map[int]string
|
||||||
keymap map[int]actionType
|
keymap map[int]actionType
|
||||||
execmap map[int]string
|
execmap map[int]string
|
||||||
@ -87,16 +94,11 @@ type Terminal struct {
|
|||||||
|
|
||||||
type selectedItem struct {
|
type selectedItem struct {
|
||||||
at time.Time
|
at time.Time
|
||||||
text string
|
item *Item
|
||||||
}
|
}
|
||||||
|
|
||||||
type byTimeOrder []selectedItem
|
type byTimeOrder []selectedItem
|
||||||
|
|
||||||
type previewRequest struct {
|
|
||||||
ok bool
|
|
||||||
str string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a byTimeOrder) Len() int {
|
func (a byTimeOrder) Len() int {
|
||||||
return len(a)
|
return len(a)
|
||||||
}
|
}
|
||||||
@ -267,6 +269,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
multi: opts.Multi,
|
multi: opts.Multi,
|
||||||
sort: opts.Sort > 0,
|
sort: opts.Sort > 0,
|
||||||
toggleSort: opts.ToggleSort,
|
toggleSort: opts.ToggleSort,
|
||||||
|
delimiter: opts.Delimiter,
|
||||||
expect: opts.Expect,
|
expect: opts.Expect,
|
||||||
keymap: opts.Keymap,
|
keymap: opts.Keymap,
|
||||||
execmap: opts.Execmap,
|
execmap: opts.Execmap,
|
||||||
@ -373,7 +376,7 @@ func (t *Terminal) output() bool {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, sel := range t.sortSelected() {
|
for _, sel := range t.sortSelected() {
|
||||||
t.printer(sel.text)
|
t.printer(sel.item.AsString(t.ansi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return found
|
return found
|
||||||
@ -912,8 +915,60 @@ func quoteEntry(entry string) string {
|
|||||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) executeCommand(template string, replacement string) {
|
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
|
||||||
command := strings.Replace(template, "{}", replacement, -1)
|
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||||
|
// Escaped pattern
|
||||||
|
if match[0] == '\\' {
|
||||||
|
return match[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current query
|
||||||
|
if match == "{q}" {
|
||||||
|
return quoteEntry(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements := make([]string, len(items))
|
||||||
|
|
||||||
|
if match == "{}" {
|
||||||
|
for idx, item := range items {
|
||||||
|
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
chars := util.RunesToChars([]rune(item.AsString(stripAnsi)))
|
||||||
|
tokens := Tokenize(chars, delimiter)
|
||||||
|
trans := Transform(tokens, ranges)
|
||||||
|
str := string(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]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
replacements[idx] = quoteEntry(str)
|
||||||
|
}
|
||||||
|
return strings.Join(replacements, " ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) executeCommand(template string, items []*Item) {
|
||||||
|
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items)
|
||||||
cmd := util.ExecCommand(command)
|
cmd := util.ExecCommand(command)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@ -931,8 +986,12 @@ func (t *Terminal) isPreviewEnabled() bool {
|
|||||||
return t.previewBox != nil && t.previewer.enabled
|
return t.previewBox != nil && t.previewer.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) currentItem() *Item {
|
||||||
|
return t.merger.Get(t.cy).item
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) current() string {
|
func (t *Terminal) current() string {
|
||||||
return t.merger.Get(t.cy).item.AsString(t.ansi)
|
return t.currentItem().AsString(t.ansi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop is called to start Terminal I/O
|
// Loop is called to start Terminal I/O
|
||||||
@ -989,18 +1048,19 @@ func (t *Terminal) Loop() {
|
|||||||
if t.hasPreviewWindow() {
|
if t.hasPreviewWindow() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
request := previewRequest{false, ""}
|
var request *Item
|
||||||
t.previewBox.Wait(func(events *util.Events) {
|
t.previewBox.Wait(func(events *util.Events) {
|
||||||
for req, value := range *events {
|
for req, value := range *events {
|
||||||
switch req {
|
switch req {
|
||||||
case reqPreviewEnqueue:
|
case reqPreviewEnqueue:
|
||||||
request = value.(previewRequest)
|
request = value.(*Item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events.Clear()
|
events.Clear()
|
||||||
})
|
})
|
||||||
if request.ok {
|
if request != nil {
|
||||||
command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1)
|
command := replacePlaceholder(t.preview.command,
|
||||||
|
t.ansi, t.delimiter, string(t.input), []*Item{request})
|
||||||
cmd := util.ExecCommand(command)
|
cmd := util.ExecCommand(command)
|
||||||
out, _ := cmd.CombinedOutput()
|
out, _ := cmd.CombinedOutput()
|
||||||
t.reqBox.Set(reqPreviewDisplay, string(out))
|
t.reqBox.Set(reqPreviewDisplay, string(out))
|
||||||
@ -1020,7 +1080,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
focused := previewRequest{false, ""}
|
var focused *Item
|
||||||
for {
|
for {
|
||||||
t.reqBox.Wait(func(events *util.Events) {
|
t.reqBox.Wait(func(events *util.Events) {
|
||||||
defer events.Clear()
|
defer events.Clear()
|
||||||
@ -1037,11 +1097,11 @@ func (t *Terminal) Loop() {
|
|||||||
case reqList:
|
case reqList:
|
||||||
t.printList()
|
t.printList()
|
||||||
cnt := t.merger.Length()
|
cnt := t.merger.Length()
|
||||||
var currentFocus previewRequest
|
var currentFocus *Item
|
||||||
if cnt > 0 && cnt > t.cy {
|
if cnt > 0 && cnt > t.cy {
|
||||||
currentFocus = previewRequest{true, t.current()}
|
currentFocus = t.currentItem()
|
||||||
} else {
|
} else {
|
||||||
currentFocus = previewRequest{false, ""}
|
currentFocus = nil
|
||||||
}
|
}
|
||||||
if currentFocus != focused {
|
if currentFocus != focused {
|
||||||
focused = currentFocus
|
focused = currentFocus
|
||||||
@ -1109,7 +1169,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
selectItem := func(item *Item) bool {
|
selectItem := func(item *Item) bool {
|
||||||
if _, found := t.selected[item.Index()]; !found {
|
if _, found := t.selected[item.Index()]; !found {
|
||||||
t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
|
t.selected[item.Index()] = selectedItem{time.Now(), item}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -1146,16 +1206,15 @@ func (t *Terminal) Loop() {
|
|||||||
case actIgnore:
|
case actIgnore:
|
||||||
case actExecute:
|
case actExecute:
|
||||||
if t.cy >= 0 && t.cy < t.merger.Length() {
|
if t.cy >= 0 && t.cy < t.merger.Length() {
|
||||||
item := t.merger.Get(t.cy).item
|
t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
|
||||||
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
|
|
||||||
}
|
}
|
||||||
case actExecuteMulti:
|
case actExecuteMulti:
|
||||||
if len(t.selected) > 0 {
|
if len(t.selected) > 0 {
|
||||||
sels := make([]string, len(t.selected))
|
sels := make([]*Item, len(t.selected))
|
||||||
for i, sel := range t.sortSelected() {
|
for i, sel := range t.sortSelected() {
|
||||||
sels[i] = quoteEntry(sel.text)
|
sels[i] = sel.item
|
||||||
}
|
}
|
||||||
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
|
t.executeCommand(t.execmap[mapkey], sels)
|
||||||
} else {
|
} else {
|
||||||
return doAction(actExecute, mapkey)
|
return doAction(actExecute, mapkey)
|
||||||
}
|
}
|
||||||
@ -1168,7 +1227,7 @@ func (t *Terminal) Loop() {
|
|||||||
t.resizeWindows()
|
t.resizeWindows()
|
||||||
cnt := t.merger.Length()
|
cnt := t.merger.Length()
|
||||||
if t.previewer.enabled && cnt > 0 && cnt > t.cy {
|
if t.previewer.enabled && cnt > 0 && cnt > t.cy {
|
||||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()})
|
t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
|
||||||
}
|
}
|
||||||
req(reqList, reqInfo)
|
req(reqList, reqInfo)
|
||||||
}
|
}
|
||||||
|
73
src/terminal_test.go
Normal file
73
src/terminal_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newItem(str string) *Item {
|
||||||
|
bytes := []byte(str)
|
||||||
|
trimmed, _, _ := extractColor(str, nil, nil)
|
||||||
|
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacePlaceholder(t *testing.T) {
|
||||||
|
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")}
|
||||||
|
items2 := []*Item{
|
||||||
|
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||||
|
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||||
|
|
||||||
|
var result string
|
||||||
|
check := func(expected string) {
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected: %s, actual: %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// {}, preserve ansi
|
||||||
|
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1)
|
||||||
|
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||||
|
|
||||||
|
// {}, strip ansi
|
||||||
|
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1)
|
||||||
|
check("echo ' foo'\\''bar baz'")
|
||||||
|
|
||||||
|
// {}, with multiple items
|
||||||
|
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2)
|
||||||
|
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||||
|
|
||||||
|
// {..}, strip leading whitespaces, preserve ansi
|
||||||
|
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1)
|
||||||
|
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||||
|
|
||||||
|
// {..}, strip leading whitespaces, strip ansi
|
||||||
|
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1)
|
||||||
|
check("echo 'foo'\\''bar baz'")
|
||||||
|
|
||||||
|
// {q}
|
||||||
|
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1)
|
||||||
|
check("echo ' foo'\\''bar baz' 'query'")
|
||||||
|
|
||||||
|
// {q}, multiple items
|
||||||
|
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2)
|
||||||
|
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||||
|
|
||||||
|
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1)
|
||||||
|
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||||
|
|
||||||
|
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "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}/'' ''")
|
||||||
|
|
||||||
|
// String delimiter
|
||||||
|
delim := "'"
|
||||||
|
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1)
|
||||||
|
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
||||||
|
|
||||||
|
// Regex delimiter
|
||||||
|
regex := regexp.MustCompile("[oa]+")
|
||||||
|
// foo'bar baz
|
||||||
|
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1)
|
||||||
|
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user