summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2016-10-03 14:16:10 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2016-10-03 14:33:28 +0900
commit3066b206af11258c06cc0b549e2056db9549855a (patch)
tree2ae46bd0811f50b8721e9444b614413e14e348a3
parent04492bab10aeb0d081eeb8806792ccb77a8c5cb6 (diff)
Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
-rw-r--r--src/terminal.go109
-rw-r--r--src/terminal_test.go73
2 files changed, 157 insertions, 25 deletions
diff --git a/src/terminal.go b/src/terminal.go
index 02fe7317..59cb8a66 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -20,6 +20,12 @@ import (
// import "github.com/pkg/profile"
+var placeholder *regexp.Regexp
+
+func init() {
+ placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
+}
+
type jumpMode int
const (
@@ -51,6 +57,7 @@ type Terminal struct {
multi bool
sort bool
toggleSort bool
+ delimiter Delimiter
expect map[int]string
keymap map[int]actionType
execmap map[int]string
@@ -87,16 +94,11 @@ type Terminal struct {
type selectedItem struct {
at time.Time
- text string
+ item *Item
}
type byTimeOrder []selectedItem
-type previewRequest struct {
- ok bool
- str string
-}
-
func (a byTimeOrder) Len() int {
return len(a)
}
@@ -267,6 +269,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
+ delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
execmap: opts.Execmap,
@@ -373,7 +376,7 @@ func (t *Terminal) output() bool {
}
} else {
for _, sel := range t.sortSelected() {
- t.printer(sel.text)
+ t.printer(sel.item.AsString(t.ansi))
}
}
return found
@@ -912,8 +915,60 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
-func (t *Terminal) executeCommand(template string, replacement string) {
- command := strings.Replace(template, "{}", replacement, -1)
+func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
+ 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.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@@ -931,8 +986,12 @@ func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewer.enabled
}
+func (t *Terminal) currentItem() *Item {
+ return t.merger.Get(t.cy).item
+}
+
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
@@ -989,18 +1048,19 @@ func (t *Terminal) Loop() {
if t.hasPreviewWindow() {
go func() {
for {
- request := previewRequest{false, ""}
+ var request *Item
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
- request = value.(previewRequest)
+ request = value.(*Item)
}
}
events.Clear()
})
- if request.ok {
- command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1)
+ if request != nil {
+ command := replacePlaceholder(t.preview.command,
+ t.ansi, t.delimiter, string(t.input), []*Item{request})
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -1020,7 +1080,7 @@ func (t *Terminal) Loop() {
}
go func() {
- focused := previewRequest{false, ""}
+ var focused *Item
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
@@ -1037,11 +1097,11 @@ func (t *Terminal) Loop() {
case reqList:
t.printList()
cnt := t.merger.Length()
- var currentFocus previewRequest
+ var currentFocus *Item
if cnt > 0 && cnt > t.cy {
- currentFocus = previewRequest{true, t.current()}
+ currentFocus = t.currentItem()
} else {
- currentFocus = previewRequest{false, ""}
+ currentFocus = nil
}
if currentFocus != focused {
focused = currentFocus
@@ -1109,7 +1169,7 @@ func (t *Terminal) Loop() {
}
selectItem := func(item *Item) bool {
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 false
@@ -1146,16 +1206,15 @@ func (t *Terminal) Loop() {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
- item := t.merger.Get(t.cy).item
- t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
+ t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
}
case actExecuteMulti:
if len(t.selected) > 0 {
- sels := make([]string, len(t.selected))
+ sels := make([]*Item, len(t.selected))
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 {
return doAction(actExecute, mapkey)
}
@@ -1168,7 +1227,7 @@ func (t *Terminal) Loop() {
t.resizeWindows()
cnt := t.merger.Length()
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)
}
diff --git a/src/terminal_test.go b/src/terminal_test.go
new file mode 100644
index 00000000..5afafaa2
--- /dev/null
+++ b/src/terminal_test.go
@@ -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'")
+}