mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 03:19:01 +00:00
parent
95c77bfb98
commit
ed57dcb924
@ -1,6 +1,14 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.16.3
|
||||
------
|
||||
- Fixed a bug where fzf incorrectly display the lines when straddling tab
|
||||
characters are trimmed
|
||||
- Placeholder expression used in `--preview` and `execute` action can
|
||||
optionally take `+` flag to be used with multiple selections
|
||||
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
|
||||
|
||||
0.16.2
|
||||
------
|
||||
- Dropped ncurses dependency
|
||||
|
@ -262,13 +262,21 @@ Execute the given command for the current line and display the result on the
|
||||
preview window. \fB{}\fR in the command is the placeholder that is replaced to
|
||||
the single-quoted string of the current line. To transform the replacement
|
||||
string, specify field index expressions between the braces (See \fBFIELD INDEX
|
||||
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current
|
||||
query string.
|
||||
EXPRESSION\fR for the details).
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --preview="head -$LINES {}"\fR
|
||||
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g. \fBfzf --multi --preview="head -10 {+}"\fR
|
||||
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string.
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
.RE
|
||||
.TP
|
||||
@ -461,7 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-multi(...)\fR (see below for the details)
|
||||
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
|
109
src/terminal.go
109
src/terminal.go
@ -24,7 +24,7 @@ import (
|
||||
var placeholder *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
|
||||
placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
|
||||
}
|
||||
|
||||
type jumpMode int
|
||||
@ -436,9 +436,9 @@ func (t *Terminal) output() bool {
|
||||
}
|
||||
found := len(t.selected) > 0
|
||||
if !found {
|
||||
cnt := t.merger.Length()
|
||||
if cnt > 0 && cnt > t.cy {
|
||||
t.printer(t.current())
|
||||
current := t.currentItem()
|
||||
if current != nil {
|
||||
t.printer(current.AsString(t.ansi))
|
||||
found = true
|
||||
}
|
||||
} else {
|
||||
@ -1044,7 +1044,27 @@ func quoteEntry(entry string) string {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
|
||||
func hasPlusFlag(template string) bool {
|
||||
for _, match := range placeholder.FindAllString(template, -1) {
|
||||
if match[0] == '\\' {
|
||||
continue
|
||||
}
|
||||
if match[1] == '+' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string {
|
||||
current := allItems[:1]
|
||||
selected := allItems[1:]
|
||||
if current[0] == nil {
|
||||
current = []*Item{}
|
||||
}
|
||||
if selected[0] == nil {
|
||||
selected = []*Item{}
|
||||
}
|
||||
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||
// Escaped pattern
|
||||
if match[0] == '\\' {
|
||||
@ -1056,6 +1076,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
|
||||
return quoteEntry(query)
|
||||
}
|
||||
|
||||
plusFlag := forcePlus
|
||||
if match[1] == '+' {
|
||||
match = "{" + match[2:]
|
||||
plusFlag = true
|
||||
}
|
||||
items := current
|
||||
if plusFlag {
|
||||
items = selected
|
||||
}
|
||||
|
||||
replacements := make([]string, len(items))
|
||||
|
||||
if match == "{}" {
|
||||
@ -1096,8 +1126,12 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Terminal) executeCommand(template string, items []*Item) {
|
||||
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items)
|
||||
func (t *Terminal) executeCommand(template string, forcePlus bool) {
|
||||
valid, list := t.buildPlusList(template, forcePlus)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
|
||||
cmd := util.ExecCommand(command)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
@ -1123,11 +1157,24 @@ func (t *Terminal) hasPreviewWindow() bool {
|
||||
}
|
||||
|
||||
func (t *Terminal) currentItem() *Item {
|
||||
return t.merger.Get(t.cy).item
|
||||
cnt := t.merger.Length()
|
||||
if cnt > 0 && cnt > t.cy {
|
||||
return t.merger.Get(t.cy).item
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Terminal) current() string {
|
||||
return t.currentItem().AsString(t.ansi)
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||
current := t.currentItem()
|
||||
if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
|
||||
return current != nil, []*Item{current, current}
|
||||
}
|
||||
sels := make([]*Item, len(t.selected)+1)
|
||||
sels[0] = current
|
||||
for i, sel := range t.sortSelected() {
|
||||
sels[i+1] = sel.item
|
||||
}
|
||||
return true, sels
|
||||
}
|
||||
|
||||
// Loop is called to start Terminal I/O
|
||||
@ -1184,19 +1231,20 @@ func (t *Terminal) Loop() {
|
||||
if t.hasPreviewer() {
|
||||
go func() {
|
||||
for {
|
||||
var request *Item
|
||||
var request []*Item
|
||||
t.previewBox.Wait(func(events *util.Events) {
|
||||
for req, value := range *events {
|
||||
switch req {
|
||||
case reqPreviewEnqueue:
|
||||
request = value.(*Item)
|
||||
request = value.([]*Item)
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if request != nil {
|
||||
// We don't display preview window if no match
|
||||
if request[0] != nil {
|
||||
command := replacePlaceholder(t.preview.command,
|
||||
t.ansi, t.delimiter, string(t.input), []*Item{request})
|
||||
t.ansi, t.delimiter, false, string(t.input), request)
|
||||
cmd := util.ExecCommand(command)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
t.reqBox.Set(reqPreviewDisplay, string(out))
|
||||
@ -1232,17 +1280,12 @@ func (t *Terminal) Loop() {
|
||||
t.printInfo()
|
||||
case reqList:
|
||||
t.printList()
|
||||
cnt := t.merger.Length()
|
||||
var currentFocus *Item
|
||||
if cnt > 0 && cnt > t.cy {
|
||||
currentFocus = t.currentItem()
|
||||
} else {
|
||||
currentFocus = nil
|
||||
}
|
||||
currentFocus := t.currentItem()
|
||||
if currentFocus != focused {
|
||||
focused = currentFocus
|
||||
if t.isPreviewEnabled() {
|
||||
t.previewBox.Set(reqPreviewEnqueue, focused)
|
||||
_, list := t.buildPlusList(t.preview.command, false)
|
||||
t.previewBox.Set(reqPreviewEnqueue, list)
|
||||
}
|
||||
}
|
||||
case reqJump:
|
||||
@ -1348,19 +1391,9 @@ func (t *Terminal) Loop() {
|
||||
switch a.t {
|
||||
case actIgnore:
|
||||
case actExecute:
|
||||
if t.cy >= 0 && t.cy < t.merger.Length() {
|
||||
t.executeCommand(a.a, []*Item{t.currentItem()})
|
||||
}
|
||||
t.executeCommand(a.a, false)
|
||||
case actExecuteMulti:
|
||||
if len(t.selected) > 0 {
|
||||
sels := make([]*Item, len(t.selected))
|
||||
for i, sel := range t.sortSelected() {
|
||||
sels[i] = sel.item
|
||||
}
|
||||
t.executeCommand(a.a, sels)
|
||||
} else {
|
||||
return doAction(action{t: actExecute, a: a.a}, mapkey)
|
||||
}
|
||||
t.executeCommand(a.a, true)
|
||||
case actInvalid:
|
||||
t.mutex.Unlock()
|
||||
return false
|
||||
@ -1369,9 +1402,11 @@ func (t *Terminal) Loop() {
|
||||
t.previewer.enabled = !t.previewer.enabled
|
||||
t.tui.Clear()
|
||||
t.resizeWindows()
|
||||
cnt := t.merger.Length()
|
||||
if t.previewer.enabled && cnt > 0 && cnt > t.cy {
|
||||
t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
|
||||
if t.previewer.enabled {
|
||||
valid, list := t.buildPlusList(t.preview.command, false)
|
||||
if valid {
|
||||
t.previewBox.Set(reqPreviewEnqueue, list)
|
||||
}
|
||||
}
|
||||
req(reqList, reqInfo, reqHeader)
|
||||
}
|
||||
|
@ -879,7 +879,7 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
|
||||
writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
|
||||
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '4/4' }
|
||||
@ -902,6 +902,43 @@ class TestGoFZF < TestBase
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + ".tmp"
|
||||
File.unlink output rescue nil
|
||||
writelines tempname, ["foo bar", "123 456"]
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
|
||||
execute = lambda do
|
||||
tmux.send_keys 'x', 'y'
|
||||
tmux.until { |lines| lines[-2].include? '0/2' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
end
|
||||
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
readonce
|
||||
|
||||
assert_equal [
|
||||
%[foo bar/foo bar/bar/bar],
|
||||
%[123 456/foo bar/456/bar],
|
||||
%[123 456 foo bar/foo bar/456 bar/bar]
|
||||
], File.readlines(output).map(&:chomp)
|
||||
rescue
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
@ -1198,7 +1235,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| lines[1].include?(' {-}') }
|
||||
@ -1212,6 +1249,17 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
|
||||
tmux.send_keys 'foobar'
|
||||
tmux.until { |lines| !lines[1].include?('{') }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| lines.match_count == 1000 }
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {3-1 }') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {4-1 3}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
|
Loading…
Reference in New Issue
Block a user