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

Compare commits

...

19 Commits

Author SHA1 Message Date
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
Junegunn Choi
8b0d0342d4 0.15.3 2016-09-29 03:05:20 +09:00
Junegunn Choi
957c12e7d7 Fix SEGV when trying to render preview but the window is closed
Close #677
2016-09-29 02:53:05 +09:00
Junegunn Choi
3b5ae0f8a2 Fix failing unit tests on ANSI attributes 2016-09-29 01:06:47 +09:00
Junegunn Choi
1fc5659842 Add support for more ANSI color attributes (#674)
Dim, underline, blink, reverse
2016-09-29 00:54:27 +09:00
Junegunn Choi
1acd2adce2 Update man page: missing actions 2016-09-26 15:33:46 +09:00
Junegunn Choi
1bc223d4b3 0.15.2 2016-09-25 22:20:43 +09:00
Junegunn Choi
bef405bfa5 Ignore VT100-related escape codes 2016-09-25 19:03:08 +09:00
Junegunn Choi
0612074abe Support high intensity colors
Close #671
2016-09-25 18:11:35 +09:00
Junegunn Choi
3bf51d8362 Merge pull request #670 from maverickwoo/fix-668
[bash-completion] Fix #668
2016-09-25 05:15:24 +09:00
Maverick Woo
2c8479a7c5 Fix #668
Handle uppercase letters in program names. This also deals with `-` and
`.`, both of which are quite common in program names, e.g., `xdg-open`
and `foo.sh`.
2016-09-24 15:39:13 -04:00
Junegunn Choi
8c8b5b313e Add preview-page-up and preview-page-down actions 2016-09-25 04:12:44 +09:00
Junegunn Choi
66d55fd893 Make preview windows scrollable
Close #669

You can use your mouse or binadble preview-up and preview-down actions
to scroll the content of the preview window.

    fzf --preview 'highlight -O ansi {}' --bind alt-j:preview-down,alt-k:preview-up
2016-09-25 02:02:00 +09:00
19 changed files with 420 additions and 132 deletions

View File

@@ -1,6 +1,35 @@
CHANGELOG CHANGELOG
========= =========
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
- Fixed race condition in `toggle-preview`
0.15.2
------
- Preview window is now scrollable
- With mouse scroll or with bindable actions
- `preview-up`
- `preview-down`
- `preview-page-up`
- `preview-page-down`
- Updated ANSI processor to support high intensity colors and ignore
some VT100-related escape sequences
0.15.1 0.15.1
------ ------
- Fixed panic when the pattern occurs after 2^15-th column - Fixed panic when the pattern occurs after 2^15-th column

View File

@@ -113,16 +113,16 @@ vim $(fzf)
Unless otherwise specified, fzf starts in "extended-search mode" where you can 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 type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!rmx` !fire`
| Token | Match type | Description | | Token | Match type | Description |
| -------- | -------------------- | -------------------------------- | | -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` | | `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` | | `'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` |
| `!'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, 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, 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 ~ :FZF ~
" With options " 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 " Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF! :FZF!

View File

@@ -2,8 +2,8 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.15.1 pre=1 || [[ "$@" =~ --pre ]] && version=0.15.4 pre=1 ||
version=0.15.1 pre=0 version=0.15.4 pre=0
auto_completion= auto_completion=
key_bindings= 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.1" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Oct 2016" "fzf 0.15.4" "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 "Sep 2016" "fzf 0.15.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Oct 2016" "fzf 0.15.4" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -232,11 +232,17 @@ automatically truncated when the number of the lines exceeds the value.
.TP .TP
.BI "--preview=" "COMMAND" .BI "--preview=" "COMMAND"
Execute the given command for the current line and display the result on the 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 preview window. \fB{}\fR in the command is the placeholder that is replaced to
current line. 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 .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
Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" .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 .SS Negation
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the 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 .SS Exact-match by default
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
@@ -434,6 +440,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBpreview-down\fR
\fBpreview-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBselect-all\fR \fBselect-all\fR
@@ -456,9 +466,11 @@ binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR \fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line. You can use the same placeholder expressions as in \fB--preview\fR.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors. 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
\fBexecute~...~\fR \fBexecute~...~\fR
@@ -477,7 +489,7 @@ alternative notations to avoid parse errors.
.RS .RS
This is the special form that frees you from parse errors as it does not expect 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 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 .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command \fBexecute-multi(...)\fR is an alternative action that executes the command

View File

@@ -558,11 +558,15 @@ let s:default_action = {
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let args = copy(a:000) let args = copy(a:000)
let opts = {} let opts = { 'options': '--multi ' }
if len(args) && isdirectory(expand(args[-1])) 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 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 endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>) command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)

View File

@@ -32,7 +32,7 @@ fi
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -117,7 +117,7 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf local cur base dir leftover matches trigger cmd fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
@@ -162,7 +162,7 @@ _fzf_complete() {
type -t "$post" > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post=cat
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]]; then
@@ -277,7 +277,7 @@ _fzf_defc() {
cmd="$1" cmd="$1"
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_$cmd" orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}" orig="${!orig_var}"
if [ -n "$orig" ]; then if [ -n "$orig" ]; then
printf -v def "$orig" "$func" printf -v def "$orig" "$func"

View File

@@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/curses"
) )
type ansiOffset struct { type ansiOffset struct {
@@ -16,24 +18,24 @@ type ansiOffset struct {
type ansiState struct { type ansiState struct {
fg int fg int
bg int bg int
bold bool attr curses.Attr
} }
func (s *ansiState) colored() bool { func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold return s.fg != -1 || s.bg != -1 || s.attr > 0
} }
func (s *ansiState) equals(t *ansiState) bool { func (s *ansiState) equals(t *ansiState) bool {
if t == nil { if t == nil {
return !s.colored() return !s.colored()
} }
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
} }
var ansiRegex *regexp.Regexp var ansiRegex *regexp.Regexp
func init() { func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b.[0-9;]*.")
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -94,11 +96,11 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State // State
var state *ansiState var state *ansiState
if prevState == nil { if prevState == nil {
state = &ansiState{-1, -1, false} state = &ansiState{-1, -1, 0}
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold} state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
} }
if ansiCode[len(ansiCode)-1] == 'K' { if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
return state return state
} }
@@ -108,7 +110,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
init := func() { init := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.bold = false state.attr = 0
state256 = 0 state256 = 0
} }
@@ -132,7 +134,15 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49: case 49:
state.bg = -1 state.bg = -1
case 1: case 1:
state.bold = true state.attr = curses.Bold
case 2:
state.attr = curses.Dim
case 4:
state.attr = curses.Underline
case 5:
state.attr = curses.Blink
case 7:
state.attr = curses.Reverse
case 0: case 0:
init() init()
default: default:
@@ -140,6 +150,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
state.fg = num - 30 state.fg = num - 30
} else if num >= 40 && num <= 47 { } else if num >= 40 && num <= 47 {
state.bg = num - 40 state.bg = num - 40
} else if num >= 90 && num <= 97 {
state.fg = num - 90 + 8
} else if num >= 100 && num <= 107 {
state.bg = num - 100 + 8
} }
} }
case 1: case 1:

View File

@@ -3,13 +3,19 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/curses"
) )
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) {
var attr curses.Attr
if bold {
attr = curses.Bold
}
if offset.offset[0] != b || offset.offset[1] != e || if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
t.Error(offset, b, e, fg, bg, bold) t.Error(offset, b, e, fg, bg, attr)
} }
} }
@@ -121,7 +127,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail() t.Fail()
} }
assert((*offsets)[0], 6, 11, 2, -1, true) assert((*offsets)[0], 6, 11, 2, -1, true)
@@ -132,7 +138,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail() t.Fail()
} }
assert((*offsets)[0], 0, 11, 2, -1, true) assert((*offsets)[0], 0, 11, 2, -1, true)
@@ -143,7 +149,7 @@ func TestExtractColor(t *testing.T) {
if len(*offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
if state.fg != 200 || state.bg != 100 || state.bold { if state.fg != 200 || state.bg != 100 || state.attr > 0 {
t.Fail() t.Fail()
} }
assert((*offsets)[0], 0, 6, 2, -1, true) assert((*offsets)[0], 0, 6, 2, -1, true)

View File

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

View File

@@ -23,6 +23,16 @@ import (
"unicode/utf8" "unicode/utf8"
) )
const (
Bold = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
type Attr C.int
// Types of user action // Types of user action
const ( const (
Rune = iota Rune = iota
@@ -158,7 +168,7 @@ type MouseEvent struct {
var ( var (
_buf []byte _buf []byte
_in *os.File _in *os.File
_color func(int, bool) C.int _color func(int, Attr) C.int
_colorMap map[int]int _colorMap map[int]int
_prevDownTime time.Time _prevDownTime time.Time
_clickY []int _clickY []int
@@ -183,7 +193,7 @@ type Window struct {
func NewWindow(top int, left int, width int, height int, border bool) *Window { 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)) win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border { if border {
attr := _color(ColBorder, false) attr := _color(ColBorder, 0)
C.wattron(win, attr) C.wattron(win, attr)
C.box(win, 0, 0) C.box(win, 0, 0)
C.wattroff(win, attr) C.wattroff(win, attr)
@@ -266,22 +276,19 @@ func init() {
Border: 145} Border: 145}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, a Attr) C.int {
var attr C.int var attr C.int
if pair > ColNormal { if pair > ColNormal {
attr = C.COLOR_PAIR(C.int(pair)) attr = C.COLOR_PAIR(C.int(pair))
} }
if bold { return attr | C.int(a)
attr = attr | C.A_BOLD
}
return attr
} }
func attrMono(pair int, bold bool) C.int { func attrMono(pair int, a Attr) C.int {
var attr C.int var attr C.int
switch pair { switch pair {
case ColCurrent: case ColCurrent:
if bold { if a&C.A_BOLD == C.A_BOLD {
attr = C.A_REVERSE attr = C.A_REVERSE
} }
case ColMatch: case ColMatch:
@@ -289,7 +296,7 @@ func attrMono(pair int, bold bool) C.int {
case ColCurrentMatch: case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE attr = C.A_UNDERLINE | C.A_REVERSE
} }
if bold { if a&C.A_BOLD == C.A_BOLD {
attr = attr | C.A_BOLD attr = attr | C.A_BOLD
} }
return attr return attr
@@ -648,8 +655,8 @@ func (w *Window) Print(text string) {
}, text))) }, text)))
} }
func (w *Window) CPrint(pair int, bold bool, text string) { func (w *Window) CPrint(pair int, a Attr, text string) {
attr := _color(pair, bold) attr := _color(pair, a)
C.wattron(w.win, attr) C.wattron(w.win, attr)
w.Print(text) w.Print(text)
C.wattroff(w.win, attr) C.wattroff(w.win, attr)
@@ -675,8 +682,8 @@ func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK return C.waddstr(w.win, C.CString(str)) == C.OK
} }
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool { func (w *Window) CFill(str string, fg int, bg int, a Attr) bool {
attr := _color(PairFor(fg, bg), bold) attr := _color(PairFor(fg, bg), a)
C.wattron(w.win, attr) C.wattron(w.win, attr)
ret := w.Fill(str) ret := w.Fill(str)
C.wattroff(w.win, attr) C.wattroff(w.win, attr)

View File

@@ -663,6 +663,14 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actTogglePreview keymap[key] = actTogglePreview
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
case "preview-up":
keymap[key] = actPreviewUp
case "preview-down":
keymap[key] = actPreviewDown
case "preview-page-up":
keymap[key] = actPreviewPageUp
case "preview-page-down":
keymap[key] = actPreviewPageDown
default: default:
if isExecuteAction(actLower) { if isExecuteAction(actLower) {
var offset int var offset int

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package fzf
import ( import (
"math" "math"
"sort" "sort"
"unicode"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -14,7 +15,7 @@ type Offset [2]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
color int color int
bold bool attr curses.Attr
index int32 index int32
} }
@@ -62,7 +63,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
for idx := 0; idx < numChars; idx++ { for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx) r := item.text.Get(idx)
whitePrefixLen = idx whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' { if idx == minBegin || !unicode.IsSpace(r) {
break break
} }
} }
@@ -91,14 +92,14 @@ func minRank() rank {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
} }
func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset { func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses.Attr, current bool) []colorOffset {
itemColors := result.item.Colors() itemColors := result.item.Colors()
if len(itemColors) == 0 { if len(itemColors) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range matchOffsets { for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
} }
return offsets return offsets
} }
@@ -142,7 +143,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool,
if curr != 0 && idx > start { if curr != 0 && idx > start {
if curr == -1 { if curr == -1 {
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
} else { } else {
ansi := itemColors[curr-1] ansi := itemColors[curr-1]
fg := ansi.color.fg fg := ansi.color.fg
@@ -164,7 +165,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool,
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) attr: ansi.color.attr | attr})
} }
} }
} }

View File

@@ -97,16 +97,20 @@ func TestColorOffset(t *testing.T) {
item := Result{ item := Result{
item: &Item{ item: &Item{
colors: &[]ansiOffset{ colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}} 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}] // [{[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, false, true) colors := item.colorOffsets(offsets, 99, 0, true)
assert := func(idx int, b int32, e int32, c int, bold bool) { assert := func(idx int, b int32, e int32, c int, bold bool) {
var attr curses.Attr
if bold {
attr = curses.Bold
}
o := colors[idx] o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
t.Error(o) t.Error(o)
} }
} }

View File

@@ -20,6 +20,12 @@ import (
// import "github.com/pkg/profile" // import "github.com/pkg/profile"
var placeholder *regexp.Regexp
func init() {
placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})")
}
type jumpMode int type jumpMode int
const ( const (
@@ -28,6 +34,13 @@ const (
jumpAcceptEnabled jumpAcceptEnabled
) )
type previewer struct {
text string
lines int
offset int
enabled bool
}
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
@@ -44,6 +57,7 @@ type Terminal struct {
multi bool multi bool
sort bool sort bool
toggleSort bool toggleSort bool
delimiter Delimiter
expect map[int]string expect map[int]string
keymap map[int]actionType keymap map[int]actionType
execmap map[int]string execmap map[int]string
@@ -68,8 +82,7 @@ type Terminal struct {
selected map[int32]selectedItem selected map[int32]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts preview previewOpts
previewing bool previewer previewer
previewTxt string
previewBox *util.EventBox previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
@@ -81,16 +94,11 @@ type Terminal struct {
type selectedItem struct { type selectedItem struct {
at time.Time at time.Time
text string item *Item
} }
type byTimeOrder []selectedItem type byTimeOrder []selectedItem
type previewRequest struct {
ok bool
str string
}
func (a byTimeOrder) Len() int { func (a byTimeOrder) Len() int {
return len(a) return len(a)
} }
@@ -119,6 +127,7 @@ const (
reqPrintQuery reqPrintQuery
reqPreviewEnqueue reqPreviewEnqueue
reqPreviewDisplay reqPreviewDisplay
reqPreviewRefresh
reqQuit reqQuit
) )
@@ -165,6 +174,10 @@ const (
actPrintQuery actPrintQuery
actToggleSort actToggleSort
actTogglePreview actTogglePreview
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviousHistory actPreviousHistory
actNextHistory actNextHistory
actExecute actExecute
@@ -256,6 +269,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi, multi: opts.Multi,
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
execmap: opts.Execmap, execmap: opts.Execmap,
@@ -275,8 +289,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
preview: opts.Preview, preview: opts.Preview,
previewing: previewBox != nil && !opts.Preview.hidden, previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
previewTxt: "",
previewBox: previewBox, previewBox: previewBox,
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
@@ -363,7 +376,7 @@ func (t *Terminal) output() bool {
} }
} else { } else {
for _, sel := range t.sortSelected() { for _, sel := range t.sortSelected() {
t.printer(sel.text) t.printer(sel.item.AsString(t.ansi))
} }
} }
return found return found
@@ -516,24 +529,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() { func (t *Terminal) printPrompt() {
t.move(0, 0, true) t.move(0, 0, true)
t.window.CPrint(C.ColPrompt, true, t.prompt) t.window.CPrint(C.ColPrompt, C.Bold, t.prompt)
t.window.CPrint(C.ColNormal, true, string(t.input)) t.window.CPrint(C.ColNormal, C.Bold, string(t.input))
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
if t.inlineInfo { if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading { if t.reading {
t.window.CPrint(C.ColSpinner, true, " < ") t.window.CPrint(C.ColSpinner, C.Bold, " < ")
} else { } else {
t.window.CPrint(C.ColPrompt, true, " < ") t.window.CPrint(C.ColPrompt, C.Bold, " < ")
} }
} else { } else {
t.move(1, 0, true) t.move(1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
t.window.CPrint(C.ColSpinner, true, _spinner[idx]) t.window.CPrint(C.ColSpinner, C.Bold, _spinner[idx])
} }
t.move(1, 2, false) t.move(1, 2, false)
} }
@@ -552,7 +565,7 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
t.window.CPrint(C.ColInfo, false, output) t.window.CPrint(C.ColInfo, 0, output)
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
@@ -576,7 +589,7 @@ func (t *Terminal) printHeader() {
colors: colors} colors: colors}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false) t.printHighlighted(&Result{item: item}, 0, C.ColHeader, 0, false)
} }
} }
@@ -610,21 +623,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) {
} else if current { } else if current {
label = ">" label = ">"
} }
t.window.CPrint(C.ColCursor, true, label) t.window.CPrint(C.ColCursor, C.Bold, label)
if current { if current {
if selected { if selected {
t.window.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, C.Bold, ">")
} else { } else {
t.window.CPrint(C.ColCurrent, true, " ") t.window.CPrint(C.ColCurrent, C.Bold, " ")
} }
t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true) t.printHighlighted(result, C.Bold, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
if selected { if selected {
t.window.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, C.Bold, ">")
} else { } else {
t.window.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(result, false, 0, C.ColMatch, false) t.printHighlighted(result, 0, 0, C.ColMatch, false)
} }
} }
@@ -680,7 +693,7 @@ func overflow(runes []rune, max int) bool {
return false return false
} }
func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) { func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 int, current bool) {
item := result.item item := result.item
// Overflow // Overflow
@@ -705,7 +718,7 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
maxe = util.Max(maxe, int(offset[1])) maxe = util.Max(maxe, int(offset[1]))
} }
offsets := result.colorOffsets(charOffsets, col2, bold, current) offsets := result.colorOffsets(charOffsets, col2, attr, current)
maxWidth := t.window.Width - 3 maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
if overflow(text, maxWidth) { if overflow(text, maxWidth) {
@@ -754,11 +767,11 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
e := util.Constrain32(offset.offset[1], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
t.window.CPrint(col1, bold, substr) t.window.CPrint(col1, attr, substr)
if b < e { if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth) substr, prefixWidth = processTabs(text[b:e], prefixWidth)
t.window.CPrint(offset.color, offset.bold, substr) t.window.CPrint(offset.color, offset.attr, substr)
} }
index = e index = e
@@ -768,18 +781,52 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in
} }
if index < maxOffset { if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = processTabs(text[index:], prefixWidth)
t.window.CPrint(col1, bold, substr) t.window.CPrint(col1, attr, substr)
} }
} }
func numLinesMax(str string, max int) int {
lines := 0
for lines < max {
idx := strings.Index(str, "\n")
if idx < 0 {
break
}
str = str[idx+1:]
lines++
}
return lines
}
func (t *Terminal) printPreview() { func (t *Terminal) printPreview() {
if !t.isPreviewEnabled() {
return
}
t.pwindow.Erase() t.pwindow.Erase()
extractColor(t.previewTxt, nil, func(str string, ansi *ansiState) bool { skip := t.previewer.offset
extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool {
if skip > 0 {
newlines := numLinesMax(str, skip)
if skip <= newlines {
for i := 0; i < skip; i++ {
str = str[strings.Index(str, "\n")+1:]
}
skip = 0
} else {
skip -= newlines
return true
}
}
if ansi != nil && ansi.colored() { if ansi != nil && ansi.colored() {
return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold) return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr)
} }
return t.pwindow.Fill(str) 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) { func processTabs(runes []rune, prefixWidth int) (string, int) {
@@ -803,10 +850,8 @@ func (t *Terminal) printAll() {
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader() t.printHeader()
if t.isPreviewEnabled() {
t.printPreview() t.printPreview()
} }
}
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
if !t.suppress { if !t.suppress {
@@ -875,8 +920,60 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
func (t *Terminal) executeCommand(template string, replacement string) { func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string {
command := strings.Replace(template, "{}", replacement, -1) return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern
if match[0] == '\\' {
return match[1:]
}
// Current query
if match == "{q}" {
return quoteEntry(query)
}
replacements := make([]string, len(items))
if match == "{}" {
for idx, item := range items {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
}
return strings.Join(replacements, " ")
}
tokens := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(tokens))
for idx, s := range tokens {
r, ok := ParseRange(&s)
if !ok {
// Invalid expression, just return the original string in the template
return match
}
ranges[idx] = r
}
for idx, item := range items {
chars := util.RunesToChars([]rune(item.AsString(stripAnsi)))
tokens := Tokenize(chars, delimiter)
trans := Transform(tokens, ranges)
str := string(joinTokens(trans))
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1)
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]]
}
}
str = strings.TrimSpace(str)
replacements[idx] = quoteEntry(str)
}
return strings.Join(replacements, " ")
})
}
func (t *Terminal) executeCommand(template string, items []*Item) {
command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -891,11 +988,15 @@ func (t *Terminal) hasPreviewWindow() bool {
} }
func (t *Terminal) isPreviewEnabled() bool { func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewing return t.previewBox != nil && t.previewer.enabled
}
func (t *Terminal) currentItem() *Item {
return t.merger.Get(t.cy).item
} }
func (t *Terminal) current() string { func (t *Terminal) current() string {
return t.merger.Get(t.cy).item.AsString(t.ansi) return t.currentItem().AsString(t.ansi)
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@@ -952,18 +1053,19 @@ func (t *Terminal) Loop() {
if t.hasPreviewWindow() { if t.hasPreviewWindow() {
go func() { go func() {
for { for {
request := previewRequest{false, ""} var request *Item
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqPreviewEnqueue: case reqPreviewEnqueue:
request = value.(previewRequest) request = value.(*Item)
} }
} }
events.Clear() events.Clear()
}) })
if request.ok { if request != nil {
command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1) command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, string(t.input), []*Item{request})
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out)) t.reqBox.Set(reqPreviewDisplay, string(out))
@@ -983,7 +1085,7 @@ func (t *Terminal) Loop() {
} }
go func() { go func() {
focused := previewRequest{false, ""} var focused *Item
for { for {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
@@ -1000,11 +1102,11 @@ func (t *Terminal) Loop() {
case reqList: case reqList:
t.printList() t.printList()
cnt := t.merger.Length() cnt := t.merger.Length()
var currentFocus previewRequest var currentFocus *Item
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
currentFocus = previewRequest{true, t.current()} currentFocus = t.currentItem()
} else { } else {
currentFocus = previewRequest{false, ""} currentFocus = nil
} }
if currentFocus != focused { if currentFocus != focused {
focused = currentFocus focused = currentFocus
@@ -1033,7 +1135,11 @@ func (t *Terminal) Loop() {
} }
exit(exitNoMatch) exit(exitNoMatch)
case reqPreviewDisplay: case reqPreviewDisplay:
t.previewTxt = value.(string) t.previewer.text = value.(string)
t.previewer.lines = strings.Count(t.previewer.text, "\n")
t.previewer.offset = 0
t.printPreview()
case reqPreviewRefresh:
t.printPreview() t.printPreview()
case reqPrintQuery: case reqPrintQuery:
C.Close() C.Close()
@@ -1068,7 +1174,7 @@ func (t *Terminal) Loop() {
} }
selectItem := func(item *Item) bool { selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found { if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)} t.selected[item.Index()] = selectedItem{time.Now(), item}
return true return true
} }
return false return false
@@ -1085,6 +1191,11 @@ func (t *Terminal) Loop() {
req(reqInfo) req(reqInfo)
} }
} }
scrollPreview := func(amount int) {
t.previewer.offset = util.Constrain(
t.previewer.offset+amount, 0, t.previewer.lines-1)
req(reqPreviewRefresh)
}
for key, ret := range t.expect { for key, ret := range t.expect {
if keyMatch(key, event) { if keyMatch(key, event) {
t.pressed = ret t.pressed = ret
@@ -1100,16 +1211,15 @@ func (t *Terminal) Loop() {
case actIgnore: case actIgnore:
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy).item t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()})
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
} }
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { if len(t.selected) > 0 {
sels := make([]string, len(t.selected)) sels := make([]*Item, len(t.selected))
for i, sel := range t.sortSelected() { for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(sel.text) sels[i] = sel.item
} }
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) t.executeCommand(t.execmap[mapkey], sels)
} else { } else {
return doAction(actExecute, mapkey) return doAction(actExecute, mapkey)
} }
@@ -1118,11 +1228,11 @@ func (t *Terminal) Loop() {
return false return false
case actTogglePreview: case actTogglePreview:
if t.hasPreviewWindow() { if t.hasPreviewWindow() {
t.previewing = !t.previewing t.previewer.enabled = !t.previewer.enabled
t.resizeWindows() t.resizeWindows()
cnt := t.merger.Length() cnt := t.merger.Length()
if t.previewing && cnt > 0 && cnt > t.cy { if t.previewer.enabled && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()}) t.previewBox.Set(reqPreviewEnqueue, t.currentItem())
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }
@@ -1131,6 +1241,22 @@ func (t *Terminal) Loop() {
t.eventBox.Set(EvtSearchNew, t.sort) t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actPreviewUp:
if t.isPreviewEnabled() {
scrollPreview(-1)
}
case actPreviewDown:
if t.isPreviewEnabled() {
scrollPreview(1)
}
case actPreviewPageUp:
if t.isPreviewEnabled() {
scrollPreview(-t.pwindow.Height)
}
case actPreviewPageDown:
if t.isPreviewEnabled() {
scrollPreview(t.pwindow.Height)
}
case actBeginningOfLine: case actBeginningOfLine:
t.cx = 0 t.cx = 0
case actBackwardChar: case actBackwardChar:
@@ -1299,6 +1425,8 @@ func (t *Terminal) Loop() {
} }
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) {
scrollPreview(-me.S)
} }
} else if t.window.Enclose(my, mx) { } else if t.window.Enclose(my, mx) {
mx -= t.window.Left mx -= t.window.Left

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