Compare commits

...

21 Commits

Author SHA1 Message Date
Junegunn Choi
9f30ca2923 0.15.5 2016-10-23 22:00:32 +09:00
Junegunn Choi
37f2d8f795 [vim] Respect g:fzf_colors
Close #711
2016-10-22 01:14:16 +09:00
Junegunn Choi
400e443a0a Make test cases less susceptible to timeout errors 2016-10-22 00:01:21 +09:00
Junegunn Choi
0a8d2996dc Set foreground color without affecting background
Close #712
2016-10-21 19:35:59 +09:00
Junegunn Choi
cfdb00b971 Allow other options to follow --color without spec 2016-10-21 19:20:16 +09:00
Junegunn Choi
9b9ad39143 [vim] Set g:loaded_fzf 2016-10-18 15:00:47 +09:00
Junegunn Choi
0541c0dbcf Use relative position instead of absolute distance for --tiebreak=end
Fix unintuitive result where `*fzf*/install` is ranked higher than
`fzf/src/fzf/*fzf*-linux_386` on --tiebreak=end.
2016-10-18 01:13:57 +09:00
Junegunn Choi
47b11cb8b4 Merge pull request #701 from nthapaliya/zsh_script_improvements
[zsh] GNU coreutils compatibility
2016-10-14 10:00:58 +09:00
Niraj Thapaliya
d3da310b92 Use command to ignore shell function 2016-10-13 09:53:24 -06:00
Niraj Thapaliya
93e0a6a9de Gnu [ evaluates both sides of a -o condition regardless
It doesn't short circuit like we expect, causing trouble when $dir is
empty

Use shell builtin instead
2016-10-13 09:52:49 -06:00
Junegunn Choi
ac549a853a [fzf-tmux] Fix bash condition
Fix #702
2016-10-13 10:42:26 +09:00
Junegunn Choi
053af9a1c8 [fzf-tmux/vim/nvim] Do not split small window
Close #699
2016-10-12 23:10:21 +09:00
Junegunn Choi
60112def02 Merge pull request #698 from Ambrevar/master
[fish] Yank commandline in fzf-history-widget
2016-10-12 01:54:51 +09:00
Pierre Neidhardt
2134c0c8a9 key-bindings.fish: Yank commandline in fzf-history-widget 2016-10-11 21:15:00 +05:30
Junegunn Choi
3222d62ddf 0.15.4 2016-10-04 02:17:36 +09:00
Junegunn Choi
aeb957a285 Use exact match by default for inverse search term
This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.

| Token    | Match type                 | Description                       |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match                | Items that match `sbtrkt`         |
| `^music` | prefix-exact-match         | Items that start with `music`     |
| `.mp3$`  | suffix-exact-match         | Items that end with `.mp3`        |
| `'wild`  | exact-match (quoted)       | Items that include `wild`         |
| `!fire`  | inverse-exact-match        | Items that do not include `fire`  |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
2016-10-04 02:09:03 +09:00
Junegunn Choi
154cf22ffa Display scroll indicator in preview window 2016-10-04 01:40:45 +09:00
Junegunn Choi
51f532697e Adjust maximum scroll offset
It was possible that a few lines at the bottom may not be visible if
there are lines above that span multiple lines.
2016-10-04 01:39:48 +09:00
Junegunn Choi
01b88539ba [vim] Apply --multi and --prompt to :FZF command 2016-10-04 00:30:04 +09:00
Junegunn Choi
3066b206af Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
2016-10-03 14:33:28 +09:00
Junegunn Choi
04492bab10 Use unicode.IsSpace to cover more whitespace characters 2016-09-29 22:40:22 +09:00
22 changed files with 372 additions and 170 deletions

View File

@@ -1,6 +1,26 @@
CHANGELOG
=========
0.15.5
------
- Setting foreground color will no longer set background color to black
- e.g. `fzf --color fg:153`
- `--tiebreak=end` will consider relative position instead of absolute distance
- Updated `fzf#wrap` function to respect `g:fzf_colors`
0.15.4
------
- Added support for range expression in preview and execute action
- e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1`
- `{q}` will be replaced to the single-quoted string of the current query
- Fixed to properly handle unicode whitespace characters
- Display scroll indicator in preview window
- Inverse search term will use exact matcher by default
- This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.
0.15.3
------
- Added support for more ANSI attributes: dim, underline, blink, and reverse

View File

@@ -113,16 +113,16 @@ vim $(fzf)
Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!rmx`
!fire`
| Token | Match type | Description |
| -------- | -------------------- | -------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` |
| `!'fire` | inverse-exact-match | Items that do not include `fire` |
| Token | Match type | Description |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` |
| `!fire` | inverse-exact-match | Items that do not include `fire` |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
If you don't prefer fuzzy matching and do not wish to "quote" every word,
start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
@@ -305,7 +305,7 @@ If you have set up fzf for Vim, `:FZF` command will be added.
:FZF ~
" With options
:FZF --no-sort -m /tmp
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
@@ -319,7 +319,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin
#### `fzf#run`
@@ -347,7 +347,8 @@ page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`.
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
```vim
command! -bang MyStuff

View File

@@ -17,6 +17,7 @@ swap=""
close=""
term=""
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols)
help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
@@ -83,7 +84,7 @@ while [[ $# -gt 0 ]]; do
else
if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then
[[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols)
max=$columns
else
max=$lines
fi
@@ -108,7 +109,7 @@ while [[ $# -gt 0 ]]; do
[[ -n "$skip" ]] && args+=("$arg")
done
if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then
if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
"$fzf" "${args[@]}"
exit $?
fi

View File

@@ -2,8 +2,8 @@
set -u
[[ "$@" =~ --pre ]] && version=0.15.3 pre=1 ||
version=0.15.3 pre=0
[[ "$@" =~ --pre ]] && version=0.15.5 pre=1 ||
version=0.15.5 pre=0
auto_completion=
key_bindings=

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
THE SOFTWARE.
..
.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.3" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Oct 2016" "fzf 0.15.5" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
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
THE SOFTWARE.
..
.TH fzf 1 "Sep 2016" "fzf 0.15.3" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Oct 2016" "fzf 0.15.5" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -232,11 +232,17 @@ automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--preview=" "COMMAND"
Execute the given command for the current line and display the result on the
preview window. \fB{}\fR is the placeholder for the quoted string of the
current line.
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.
.RS
e.g. \fBfzf --preview="head -$LINES {}"\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
Note that you can escape a placeholder pattern by prepending a backslash.
.RE
.TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]"
@@ -358,7 +364,7 @@ with the given string. An anchored-match term is also an exact-match term.
.SS Negation
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
term from the result.
term from the result. In this case, fzf performs exact match by default.
.SS Exact-match by default
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
@@ -460,9 +466,11 @@ binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
You can use the same placeholder expressions as in \fB--preview\fR.
If the command contains parentheses, fzf may fail to parse the expression. In
that case, you can use any of the following alternative notations to avoid
parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
@@ -481,7 +489,7 @@ alternative notations to avoid parse errors.
.RS
This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list.
comma-separated list of key-action pairs.
.RE
\fBexecute-multi(...)\fR is an alternative action that executes the command

View File

@@ -21,6 +21,11 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
if exists('g:loaded_fzf')
finish
endif
let g:loaded_fzf = 1
let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
@@ -154,6 +159,22 @@ function! s:common_sink(action, lines) abort
endtry
endfunction
function! s:get_color(attr, ...)
for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm')
if code =~ '^[0-9]\+$'
return code
endif
endfor
return ''
endfunction
function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors)
endfunction
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
@@ -185,8 +206,10 @@ function! fzf#wrap(...)
endif
endif
" Colors: g:fzf_colors
let opts.options = s:defaults() .' '. get(opts, 'options', '')
" History: g:fzf_history_dir
let opts.options = get(opts, 'options', '')
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir)
if !isdirectory(dir)
@@ -289,7 +312,8 @@ function! s:fzf_tmux(dict)
endfunction
function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right')
return s:present(a:dict, 'up', 'down') && &lines > 15 ||
\ s:present(a:dict, 'left', 'right') && &columns > 40
endfunction
function! s:pushd(dict)
@@ -405,24 +429,25 @@ function! s:split(dict)
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let ppos = s:getpos()
try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val, a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
if s:present(a:dict, 'window')
execute a:dict.window
else
elseif !s:splittable(a:dict)
execute (tabpagenr()-1).'tabnew'
else
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val, a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
endif
return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
finally
@@ -558,11 +583,15 @@ let s:default_action = {
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = {}
let opts = { 'options': '--multi ' }
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '')
let opts.options .= ' --prompt '.shellescape(opts.dir)
else
let opts.options .= ' --prompt '.shellescape(pathshorten(getcwd()).'/')
endif
call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
let opts.options .= ' '.join(args)
call fzf#run(fzf#wrap('FZF', opts, a:bang))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)

View File

@@ -44,7 +44,7 @@ __fzf_generic_path_completion() {
setopt localoptions nonomatch
dir="$base"
while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then
if [[ -z "$dir" || -d ${~dir} ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
@@ -111,7 +111,7 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u

View File

@@ -26,8 +26,8 @@ function fzf_key_bindings
end
function fzf-history-widget
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q '(commandline)' > $TMPDIR/fzf.result
and commandline -- (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end

View File

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

View File

@@ -113,7 +113,8 @@ const (
// Pallete
const (
ColNormal = iota
_ = iota
ColNormal
ColPrompt
ColMatch
ColCurrent
@@ -134,7 +135,6 @@ const (
)
type ColorTheme struct {
UseDefault bool
Fg int16
Bg int16
DarkBg int16
@@ -168,7 +168,8 @@ type MouseEvent struct {
var (
_buf []byte
_in *os.File
_color func(int, Attr) C.int
_color bool
_colorFn func(int, Attr) C.int
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
@@ -176,10 +177,6 @@ var (
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
FG int
CurrentFG int
BG int
DarkBG int
)
type Window struct {
@@ -192,12 +189,16 @@ type Window struct {
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if _color {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(ColNormal)))
}
if border {
attr := _color(ColBorder, 0)
attr := _colorFn(ColBorder, 0)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
@@ -209,7 +210,6 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window {
func EmptyTheme() *ColorTheme {
return &ColorTheme{
UseDefault: true,
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
@@ -230,9 +230,8 @@ func init() {
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
Fg: colDefault,
Bg: colDefault,
DarkBg: C.COLOR_BLACK,
Prompt: C.COLOR_BLUE,
Match: C.COLOR_GREEN,
@@ -245,9 +244,8 @@ func init() {
Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
Fg: colDefault,
Bg: colDefault,
DarkBg: 236,
Prompt: 110,
Match: 108,
@@ -260,9 +258,8 @@ func init() {
Header: 109,
Border: 59}
Light256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
Fg: colDefault,
Bg: colDefault,
DarkBg: 251,
Prompt: 25,
Match: 66,
@@ -278,7 +275,7 @@ func init() {
func attrColored(pair int, a Attr) C.int {
var attr C.int
if pair > ColNormal {
if pair > 0 {
attr = C.COLOR_PAIR(C.int(pair))
}
return attr | C.int(a)
@@ -344,7 +341,8 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.noecho()
C.raw() // stty dsusp undef
if theme != nil {
_color = theme != nil
if _color {
C.start_color()
var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
@@ -353,52 +351,57 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored
C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal)))
_colorFn = attrColored
} else {
_color = attrMono
_colorFn = attrMono
}
}
func override(a int16, b int16) C.short {
if b == colUndefined {
return C.short(a)
func override(baseTheme *ColorTheme, theme *ColorTheme) {
o := func(a int16, b int16) int16 {
if b == colUndefined {
return a
}
return b
}
return C.short(b)
theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
fg := override(baseTheme.Fg, theme.Fg)
bg := override(baseTheme.Bg, theme.Bg)
if black {
bg = C.COLOR_BLACK
} else if theme.UseDefault {
fg = colDefault
bg = colDefault
C.use_default_colors()
}
if theme.UseDefault {
FG = colDefault
BG = colDefault
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
theme.Bg = C.COLOR_BLACK
}
// Updates theme
override(baseTheme, theme)
currentFG := override(baseTheme.Current, theme.Current)
darkBG := override(baseTheme.DarkBg, theme.DarkBg)
CurrentFG = int(currentFG)
DarkBG = int(darkBG)
C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
initPair := func(group C.short, fg int16, bg int16) {
C.init_pair(group, C.short(fg), C.short(bg))
}
initPair(ColNormal, theme.Fg, theme.Bg)
initPair(ColPrompt, theme.Prompt, theme.Bg)
initPair(ColMatch, theme.Match, theme.Bg)
initPair(ColCurrent, theme.Current, theme.DarkBg)
initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg)
initPair(ColSpinner, theme.Spinner, theme.Bg)
initPair(ColInfo, theme.Info, theme.Bg)
initPair(ColCursor, theme.Cursor, theme.DarkBg)
initPair(ColSelected, theme.Selected, theme.DarkBg)
initPair(ColHeader, theme.Header, theme.Bg)
initPair(ColBorder, theme.Border, theme.Bg)
}
func Close() {
@@ -656,7 +659,7 @@ func (w *Window) Print(text string) {
}
func (w *Window) CPrint(pair int, a Attr, text string) {
attr := _color(pair, a)
attr := _colorFn(pair, a)
C.wattron(w.win, attr)
w.Print(text)
C.wattroff(w.win, attr)
@@ -683,7 +686,7 @@ func (w *Window) Fill(str string) bool {
}
func (w *Window) CFill(str string, fg int, bg int, a Attr) bool {
attr := _color(PairFor(fg, bg), a)
attr := _colorFn(PairFor(fg, bg), a)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)

View File

@@ -246,7 +246,7 @@ func nextString(args []string, i *int, message string) string {
}
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") {
*i++
return args[*i]
}
@@ -499,10 +499,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":

View File

@@ -299,20 +299,14 @@ func TestColorSpec(t *testing.T) {
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
if *curses.Dark256 != *customized {
t.Errorf("colors should now be equivalent: %v, %v", curses.Dark256, customized)
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
}
func TestParseNilTheme(t *testing.T) {

View File

@@ -163,12 +163,13 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
if strings.HasPrefix(text, "!") {
inv = true
typ = termExact
text = text[1:]
}
if strings.HasPrefix(text, "'") {
// Flip exactness
if fuzzy {
if fuzzy && !inv {
typ = termExact
text = text[1:]
} else {

View File

@@ -22,15 +22,15 @@ func TestParseTermsExtended(t *testing.T) {
terms[1][0].typ != termExact || terms[1][0].inv ||
terms[2][0].typ != termPrefix || terms[2][0].inv ||
terms[3][0].typ != termSuffix || terms[3][0].inv ||
terms[4][0].typ != termFuzzy || !terms[4][0].inv ||
terms[5][0].typ != termExact || !terms[5][0].inv ||
terms[4][0].typ != termExact || !terms[4][0].inv ||
terms[5][0].typ != termFuzzy || !terms[5][0].inv ||
terms[6][0].typ != termPrefix || !terms[6][0].inv ||
terms[7][0].typ != termSuffix || !terms[7][0].inv ||
terms[7][1].typ != termEqual || terms[7][1].inv ||
terms[8][0].typ != termPrefix || terms[8][0].inv ||
terms[8][1].typ != termExact || terms[8][1].inv ||
terms[8][2].typ != termSuffix || terms[8][2].inv ||
terms[8][3].typ != termFuzzy || !terms[8][3].inv {
terms[8][3].typ != termExact || !terms[8][3].inv {
t.Errorf("%s", terms)
}
for idx, termSet := range terms[:8] {

View File

@@ -3,6 +3,7 @@ package fzf
import (
"math"
"sort"
"unicode"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
@@ -56,21 +57,21 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
case byLength:
// If offsets is empty, trimLen will be 0, but we don't care
val = util.AsUint16(trimLen)
case byBegin:
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
if idx == minBegin || !unicode.IsSpace(r) {
break
}
}
val = util.AsUint16(minBegin - whitePrefixLen)
}
case byEnd:
if validOffsetFound {
val = util.AsUint16(1 + numChars - maxEnd)
if criterion == byBegin {
val = util.AsUint16(minBegin - whitePrefixLen)
} else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
}
}
}
result.rank.points[idx] = val
@@ -91,13 +92,13 @@ func minRank() rank {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses.Attr, current bool) []colorOffset {
func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, current bool) []colorOffset {
itemColors := result.item.Colors()
// No ANSI code, or --color=no
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
}
return offsets
@@ -148,17 +149,17 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
fg = int(theme.Current)
} else {
fg = curses.FG
fg = int(theme.Fg)
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
bg = int(theme.DarkBg)
} else {
bg = curses.BG
bg = int(theme.Bg)
}
}
colors = append(colors, colorOffset{

View File

@@ -103,7 +103,7 @@ func TestColorOffset(t *testing.T) {
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, curses.Bold}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
colors := item.colorOffsets(offsets, 99, 0, true)
colors := item.colorOffsets(offsets, curses.Dark256, 99, 0, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
var attr curses.Attr
if bold {

View File

@@ -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
@@ -83,20 +90,16 @@ type Terminal struct {
suppress bool
startChan chan bool
slab *util.Slab
theme *C.ColorTheme
}
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 +270,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,
@@ -292,6 +296,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
mutex: sync.Mutex{},
suppress: true,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Theme, opts.Black, opts.Mouse)
@@ -373,7 +378,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
@@ -634,7 +639,7 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else {
t.window.Print(" ")
}
t.printHighlighted(result, 0, 0, C.ColMatch, false)
t.printHighlighted(result, 0, C.ColNormal, C.ColMatch, false)
}
}
@@ -715,7 +720,7 @@ func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2
maxe = util.Max(maxe, int(offset[1]))
}
offsets := result.colorOffsets(charOffsets, col2, attr, current)
offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current)
maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
if overflow(text, maxWidth) {
@@ -819,6 +824,11 @@ func (t *Terminal) printPreview() {
}
return t.pwindow.Fill(str)
})
if t.previewer.offset > 0 {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
t.pwindow.Move(0, t.pwindow.Width-len(offset))
t.pwindow.CPrint(C.ColInfo, C.Reverse, offset)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
@@ -912,8 +922,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 +993,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 +1055,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 +1087,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 +1104,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 +1176,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
@@ -1128,7 +1195,7 @@ func (t *Terminal) Loop() {
}
scrollPreview := func(amount int) {
t.previewer.offset = util.Constrain(
t.previewer.offset+amount, 0, t.previewer.lines-t.pwindow.Height)
t.previewer.offset+amount, 0, t.previewer.lines-1)
req(reqPreviewRefresh)
}
for key, ret := range t.expect {
@@ -1146,16 +1213,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 +1234,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)
}

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

View File

@@ -1,6 +1,7 @@
package util
import (
"unicode"
"unicode/utf8"
)
@@ -63,7 +64,7 @@ func (chars *Chars) TrimLength() int {
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
}
@@ -75,7 +76,7 @@ func (chars *Chars) TrimLength() int {
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
}
@@ -86,7 +87,7 @@ func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
if !unicode.IsSpace(char) {
break
}
whitespaces++

View File

@@ -143,6 +143,10 @@ Execute (fzf#wrap):
Assert opts.options =~ '--history /tmp/foobar'
Assert opts.options =~ '--color light'
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
let opts = fzf#wrap({})
Assert opts.options =~ '^--color=fg:'
Execute (Cleanup):
unlet g:dir
Restore

View File

@@ -136,8 +136,10 @@ class Tmux
def prepare
tries = 0
begin
self.send_keys 'C-u', 'hello', 'Right'
self.until { |lines| lines[-1].end_with?('hello') }
self.until do |lines|
self.send_keys 'C-u', 'hello'
lines[-1].end_with?('hello')
end
rescue Exception
(tries += 1) < 5 ? retry : raise
end
@@ -604,8 +606,8 @@ class TestGoFZF < TestBase
], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/)
assert_equal [
' xxxxoxxx',
'xxxxxoxxx',
' xxxxoxxx',
'xxxxoxxxx',
'xxxoxxxxxx',
'xxoxxxxxxx',