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:
Junegunn Choi 2016-10-03 14:16:10 +09:00
parent 04492bab10
commit 3066b206af
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
2 changed files with 157 additions and 25 deletions

View File

@ -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
View 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'")
}