m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 06:13:47 -05:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Junegunn Choi
d85a69a709 0.16.3 2017-01-30 01:53:17 +09:00
Junegunn Choi
a425e96fb2 [vim] g:fzf_prefer_tmux for choosing fzf-tmux over --height
https://github.com/junegunn/fzf.vim/issues/296
2017-01-30 01:47:30 +09:00
Junegunn Choi
7763fdf6ba Update man pages 2017-01-30 01:27:12 +09:00
Junegunn Choi
dd156b59fc Fix display issues with execute action
- Move cursor to the top-left corner when starting a command in
  alternate screen
- Fix cursor position when returning to alternate screen when fzf is
  running in full screen mode
2017-01-30 01:08:07 +09:00
Junegunn Choi
36dceecd58 Add support for ctrl-space key
Close #825
2017-01-28 02:54:47 +09:00
Junegunn Choi
421b9b271a Add execute-silent action
Close #823
2017-01-27 18:56:41 +09:00
Junegunn Choi
ed57dcb924 Extend placeholder expression for multiple selections
Close #788
2017-01-27 16:38:42 +09:00
Junegunn Choi
95c77bfb98 Use --bind instead of --toggle-sort
Related #822
2017-01-26 11:54:08 +09:00
Junegunn Choi
2e3e721344 Merge branch 'devel' 2017-01-26 11:52:24 +09:00
Junegunn Choi
da2c28d5c2 Add --read0 and --print0 to --help output
Close #822
2017-01-26 11:41:20 +09:00
Junegunn Choi
dbddee9de9 [fish] Add toggle-sort back to CTRL-R (#759) 2017-01-25 10:21:14 +09:00
Junegunn Choi
8731d75607 Recalculate the width of trimmed line
Close #821
2017-01-25 02:39:49 +09:00
19 changed files with 274 additions and 114 deletions

View File

@@ -1,6 +1,19 @@
CHANGELOG 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}'`
- Added `execute-silent` action for executing a command silently without
switching to the alternate screen. This is useful when the process is
short-lived and you're not interested in its output.
- e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'`
- `ctrl-space` is allowed in `--bind`
0.16.2 0.16.2
------ ------
- Dropped ncurses dependency - Dropped ncurses dependency

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.16.2 version=0.16.3
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.2" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jan 2017" "fzf 0.16.3" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jan 2017" "fzf 0.16.2" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2017" "fzf 0.16.3" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -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 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 the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX 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 EXPRESSION\fR for the details).
query string.
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\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. Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
@@ -323,10 +331,10 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0" .B "--read0"
Read input delimited by ASCII NUL character instead of newline character Read input delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--print0" .B "--print0"
Print output delimited by ASCII NUL character instead of newline character Print output delimited by ASCII NUL characters instead of newline characters
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
@@ -418,6 +426,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS) .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIctrl-space\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIalt-[0-9]\fR \fIalt-[0-9]\fR
\fIf[1-12]\fR \fIf[1-12]\fR
@@ -461,7 +470,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -487,7 +497,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
@@ -530,10 +540,12 @@ the closing character. The catch is that it should be the last one in the
comma-separated list of key-action pairs. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command fzf switches to the alternate screen when executing a command. However, if the
with the selected entries when multi-select is enabled (\fB--multi\fR). With command is expected to complete quickly, and you are not interested in its
this action, \fB{}\fR is replaced with the quoted strings of the selected output, you might want to use \fBexecute-silent\fR instead, which silently
entries separated by spaces. executes the command without the switching. Note that fzf will not be
responsible until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR).
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2016 Junegunn Choi " Copyright (c) 2017 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -301,23 +301,28 @@ try
let prefix = '' let prefix = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && let use_height = has_key(dict, 'down') &&
\ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) &&
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) let use_term = has('nvim')
let term = has('nvim') && !tmux let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
if use_height if use_height
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
elseif term elseif use_term
let optstr .= ' --no-height' let optstr .= ' --no-height'
endif endif
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let lines = tmux ? s:execute_tmux(dict, command, temps) let lines = use_tmux ? s:execute_tmux(dict, command, temps)
\ : s:execute(dict, command, use_height, temps) \ : s:execute(dict, command, use_height, temps)
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines

View File

@@ -56,7 +56,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
command grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"

View File

@@ -45,7 +45,7 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index $FZF_CTRL_R_OPTS +m" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
history | eval (__fzfcmd) -q '(commandline)' | read -l result history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end

View File

@@ -55,7 +55,7 @@ fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst pipefail 2> /dev/null setopt localoptions noglobsubst pipefail 2> /dev/null
selected=( $(fc -l 1 | selected=( $(fc -l 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.16.2" version = "0.16.3"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -82,6 +82,8 @@ const usage = `usage: fzf [options]
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
Environment variables Environment variables
@@ -391,6 +393,8 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = tui.BSpace chord = tui.BSpace
case "ctrl-space":
chord = tui.CtrlSpace
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = tui.AltEnter chord = tui.AltEnter
case "alt-space": case "alt-space":
@@ -579,18 +583,25 @@ const (
escapedPlus = 2 escapedPlus = 2
) )
func parseKeymap(keymap map[int][]action, str string) { func init() {
if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
"(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") "(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
} }
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if src[len(":execute")] == '-' { prefix := ":execute"
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
} }
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
@@ -726,9 +737,12 @@ func parseKeymap(keymap map[int][]action, str string) {
errorExit("unknown action: " + spec) errorExit("unknown action: " + spec)
} else { } else {
var offset int var offset int
if t == actExecuteMulti { switch t {
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi") offset = len("execute-multi")
} else { default:
offset = len("execute") offset = len("execute")
} }
if spec[offset] == ':' { if spec[offset] == ':' {
@@ -750,23 +764,21 @@ func parseKeymap(keymap map[int][]action, str string) {
} }
func isExecuteAction(str string) actionType { func isExecuteAction(str string) actionType {
t := actExecute matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { if matches == nil || len(matches) != 1 {
return actIgnore return actIgnore
} }
prefix := matches[0][1]
b := str[len("execute")] if len(prefix) == 0 {
if strings.HasPrefix(str, "execute-multi") { prefix = matches[0][2]
if len(str) < len("execute-multi(") {
return actIgnore
} }
t = actExecuteMulti switch prefix {
b = str[len("execute-multi")] case "execute":
} return actExecute
e := str[len(str)-1] case "execute-silent":
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || return actExecuteSilent
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { case "execute-multi":
return t return actExecuteMulti
} }
return actIgnore return actIgnore
} }

View File

@@ -24,7 +24,7 @@ import (
var placeholder *regexp.Regexp var placeholder *regexp.Regexp
func init() { func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})") placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
} }
type jumpMode int type jumpMode int
@@ -204,7 +204,8 @@ const (
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
actExecuteMulti actExecuteSilent
actExecuteMulti // Deprecated
) )
func toActions(types ...actionType) []action { func toActions(types ...actionType) []action {
@@ -436,9 +437,9 @@ func (t *Terminal) output() bool {
} }
found := len(t.selected) > 0 found := len(t.selected) > 0
if !found { if !found {
cnt := t.merger.Length() current := t.currentItem()
if cnt > 0 && cnt > t.cy { if current != nil {
t.printer(t.current()) t.printer(current.AsString(t.ansi))
found = true found = true
} }
} else { } else {
@@ -847,6 +848,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} }
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
} }
var index int32 var index int32
@@ -1043,7 +1045,27 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" 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 { return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern // Escaped pattern
if match[0] == '\\' { if match[0] == '\\' {
@@ -1055,6 +1077,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
return quoteEntry(query) return quoteEntry(query)
} }
plusFlag := forcePlus
if match[1] == '+' {
match = "{" + match[2:]
plusFlag = true
}
items := current
if plusFlag {
items = selected
}
replacements := make([]string, len(items)) replacements := make([]string, len(items))
if match == "{}" { if match == "{}" {
@@ -1095,18 +1127,27 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu
}) })
} }
func (t *Terminal) executeCommand(template string, items []*Item) { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) {
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items) 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 := util.ExecCommand(command)
if !background {
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
t.tui.Pause() t.tui.Pause()
cmd.Run() cmd.Run()
if t.tui.Resume() { if t.tui.Resume() {
t.tui.Clear()
t.printAll() t.printAll()
} }
t.refresh() t.refresh()
} else {
cmd.Run()
}
} }
func (t *Terminal) hasPreviewer() bool { func (t *Terminal) hasPreviewer() bool {
@@ -1122,11 +1163,24 @@ func (t *Terminal) hasPreviewWindow() bool {
} }
func (t *Terminal) currentItem() *Item { func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item return t.merger.Get(t.cy).item
} }
return nil
}
func (t *Terminal) current() string { func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
return t.currentItem().AsString(t.ansi) 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 // Loop is called to start Terminal I/O
@@ -1183,19 +1237,20 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() { if t.hasPreviewer() {
go func() { go func() {
for { for {
var request *Item 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.(*Item) request = value.([]*Item)
} }
} }
events.Clear() events.Clear()
}) })
if request != nil { // We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command, 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) cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out)) t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -1231,17 +1286,12 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length() currentFocus := t.currentItem()
var currentFocus *Item
if cnt > 0 && cnt > t.cy {
currentFocus = t.currentItem()
} else {
currentFocus = nil
}
if currentFocus != focused { if currentFocus != focused {
focused = currentFocus focused = currentFocus
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused) _, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, list)
} }
} }
case reqJump: case reqJump:
@@ -1346,20 +1396,10 @@ func (t *Terminal) Loop() {
doAction = func(a action, mapkey int) bool { doAction = func(a action, mapkey int) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute: case actExecute, actExecuteSilent:
if t.cy >= 0 && t.cy < t.merger.Length() { t.executeCommand(a.a, false, a.t == actExecuteSilent)
t.executeCommand(a.a, []*Item{t.currentItem()})
}
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { t.executeCommand(a.a, true, false)
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)
}
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
@@ -1368,9 +1408,11 @@ func (t *Terminal) Loop() {
t.previewer.enabled = !t.previewer.enabled t.previewer.enabled = !t.previewer.enabled
t.tui.Clear() t.tui.Clear()
t.resizeWindows() t.resizeWindows()
cnt := t.merger.Length() if t.previewer.enabled {
if t.previewer.enabled && cnt > 0 && cnt > t.cy { valid, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) if valid {
t.previewBox.Set(reqPreviewEnqueue, list)
}
} }
req(reqList, reqInfo, reqHeader) req(reqList, reqInfo, reqHeader)
} }

View File

@@ -14,8 +14,10 @@ func newItem(str string) *Item {
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{ items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
@@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) {
} }
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz'") check("echo ' foo'\\''bar baz'")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar baz'") check("echo 'foo'\\''bar baz'")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'") check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") 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) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") 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) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "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}/'' ''") check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "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}/'' ''")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
// String delimiter // String delimiter
delim := "'" delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1) result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter // Regex delimiter
regex := regexp.MustCompile("[oa]+") regex := regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
} }

View File

@@ -322,6 +322,8 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case ESC: case ESC:
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
@@ -532,6 +534,7 @@ func (r *LightRenderer) Pause() {
r.rmcup() r.rmcup()
} else { } else {
r.smcup() r.smcup()
r.csi("H")
} }
r.flush() r.flush()
} }

View File

@@ -475,6 +475,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return escSequence() return escSequence()
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
} }
// CTRL-A ~ CTRL-Z // CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ { if c >= CtrlA && c <= CtrlZ {

View File

@@ -270,6 +270,8 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlY, 0, nil} return Event{CtrlY, 0, nil}
case tcell.KeyCtrlZ: case tcell.KeyCtrlZ:
return Event{CtrlZ, 0, nil} return Event{CtrlZ, 0, nil}
case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyBackspace, tcell.KeyBackspace2: case tcell.KeyBackspace, tcell.KeyBackspace2:
if alt { if alt {
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}

View File

@@ -38,6 +38,7 @@ const (
CtrlY CtrlY
CtrlZ CtrlZ
ESC ESC
CtrlSpace
Invalid Invalid
Resize Resize

View File

@@ -879,7 +879,7 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-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] writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '4/4' } tmux.until { |lines| lines[-2].include? '4/4' }
@@ -902,6 +902,43 @@ class TestGoFZF < TestBase
File.unlink output rescue nil File.unlink output rescue nil
end 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 def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
@@ -1198,7 +1235,7 @@ class TestGoFZF < TestBase
end end
def test_preview 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.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| lines[1].include?(' {-}') } tmux.until { |lines| lines[1].include?(' {-}') }
@@ -1212,6 +1249,17 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].start_with? ' 28/1000' } tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
tmux.send_keys 'foobar' tmux.send_keys 'foobar'
tmux.until { |lines| !lines[1].include?('{') } 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 end
def test_preview_hidden def test_preview_hidden