mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 06:43:47 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3222d62ddf | ||
|
|
aeb957a285 | ||
|
|
154cf22ffa | ||
|
|
51f532697e | ||
|
|
01b88539ba | ||
|
|
3066b206af | ||
|
|
04492bab10 | ||
|
|
8b0d0342d4 | ||
|
|
957c12e7d7 | ||
|
|
3b5ae0f8a2 | ||
|
|
1fc5659842 | ||
|
|
1acd2adce2 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +1,24 @@
|
|||||||
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
|
0.15.2
|
||||||
------
|
------
|
||||||
- Preview window is now scrollable
|
- Preview window is now scrollable
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -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!
|
||||||
|
|||||||
4
install
4
install
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
[[ "$@" =~ --pre ]] && version=0.15.2 pre=1 ||
|
[[ "$@" =~ --pre ]] && version=0.15.4 pre=1 ||
|
||||||
version=0.15.2 pre=0
|
version=0.15.4 pre=0
|
||||||
|
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
|
|||||||
@@ -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.2" "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
|
||||||
|
|||||||
@@ -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.2" "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
|
||||||
|
|||||||
@@ -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>)
|
||||||
|
|||||||
24
src/ansi.go
24
src/ansi.go
@@ -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,18 +18,18 @@ 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
|
||||||
@@ -94,9 +96,9 @@ 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[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Current version
|
// Current version
|
||||||
version = "0.15.2"
|
version = "0.15.4"
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
src/terminal.go
159
src/terminal.go
@@ -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 (
|
||||||
@@ -51,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
|
||||||
@@ -87,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)
|
||||||
}
|
}
|
||||||
@@ -267,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,
|
||||||
@@ -373,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
|
||||||
@@ -526,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)
|
||||||
}
|
}
|
||||||
@@ -562,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() {
|
||||||
@@ -586,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,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
|
||||||
@@ -715,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) {
|
||||||
@@ -764,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
|
||||||
@@ -778,7 +781,7 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,6 +799,9 @@ func numLinesMax(str string, max int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPreview() {
|
func (t *Terminal) printPreview() {
|
||||||
|
if !t.isPreviewEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
t.pwindow.Erase()
|
t.pwindow.Erase()
|
||||||
skip := t.previewer.offset
|
skip := t.previewer.offset
|
||||||
extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool {
|
extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool {
|
||||||
@@ -812,10 +818,15 @@ func (t *Terminal) printPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@@ -839,9 +850,7 @@ 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() {
|
||||||
@@ -911,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
|
||||||
@@ -930,8 +991,12 @@ func (t *Terminal) isPreviewEnabled() bool {
|
|||||||
return t.previewBox != nil && t.previewer.enabled
|
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
|
||||||
@@ -988,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))
|
||||||
@@ -1019,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()
|
||||||
@@ -1036,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
|
||||||
@@ -1108,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
|
||||||
@@ -1127,7 +1193,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
scrollPreview := func(amount int) {
|
scrollPreview := func(amount int) {
|
||||||
t.previewer.offset = util.Constrain(
|
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)
|
req(reqPreviewRefresh)
|
||||||
}
|
}
|
||||||
for key, ret := range t.expect {
|
for key, ret := range t.expect {
|
||||||
@@ -1145,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)
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1232,7 @@ func (t *Terminal) Loop() {
|
|||||||
t.resizeWindows()
|
t.resizeWindows()
|
||||||
cnt := t.merger.Length()
|
cnt := t.merger.Length()
|
||||||
if t.previewer.enabled && 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)
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/terminal_test.go
Normal file
73
src/terminal_test.go
Normal 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'")
|
||||||
|
}
|
||||||
@@ -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++
|
||||||
|
|||||||
Reference in New Issue
Block a user