mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-29 11:12:00 -07:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
34fe5ab143 | ||
|
1b08f43f82 | ||
|
b24a2e2fdc | ||
|
4c4c6e626e | ||
|
7310370a31 | ||
|
8ae94f0059 | ||
|
8fccf20892 | ||
|
5a874ae241 | ||
|
f4e1ed25f2 | ||
|
cbfbb49ab4 | ||
|
489b16efce | ||
|
b82c1693c0 | ||
|
019bfc4e35 | ||
|
dfda5c054a | ||
|
f657169616 | ||
|
4c06da8b70 | ||
|
9fe2393a00 | ||
|
e2e8d94b14 | ||
|
4f9a7f8c87 | ||
|
bb0502ff44 | ||
|
c256442245 | ||
|
1137404190 | ||
|
d57c6d0284 | ||
|
76bbf57b3d | ||
|
806a47a7cc | ||
|
29851c18aa | ||
|
dea950c2c8 | ||
|
a367dfb22e | ||
|
9fe1a7b373 | ||
|
8e2d21c548 | ||
|
bedf1cd357 | ||
|
13f180a70c |
5
.github/workflows/linux.yml
vendored
5
.github/workflows/linux.yml
vendored
@@ -23,16 +23,13 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: fish-actions/install-fish
|
||||
uses: fish-actions/install-fish@v1.0.0
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh tmux
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
|
11
.gon.hcl
11
.gon.hcl
@@ -1,11 +0,0 @@
|
||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
|
||||
sign {
|
||||
application_identity = "Apple Development: junegunn.c@gmail.com"
|
||||
}
|
@@ -12,10 +12,25 @@ builds:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: gon .gon.hcl
|
||||
post: |-
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon.hcl << EOF
|
||||
source = ["./dist/fzf-macos_darwin_{{ .Arch }}/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
sign {
|
||||
application_identity = "Apple Development: junegunn.c@gmail.com"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon.hcl
|
||||
'
|
||||
|
||||
- goos:
|
||||
- linux
|
||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,6 +1,34 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.26.0
|
||||
------
|
||||
- Added support for fixed header in preview window
|
||||
```sh
|
||||
# Display top 3 lines as the fixed header
|
||||
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
|
||||
```
|
||||
- More advanced preview offset expression to better support the fixed header
|
||||
```sh
|
||||
# Preview with bat, matching line in the middle of the window below
|
||||
# the fixed header of the top 3 lines
|
||||
#
|
||||
# ~3 Top 3 lines as the fixed header
|
||||
# +{2} Base scroll offset extracted from the second field
|
||||
# +3 Extra offset to compensate for the 3-line header
|
||||
# /2 Put in the middle of the preview area
|
||||
#
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : \
|
||||
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
|
||||
--preview-window '~3:+{2}+3/2'
|
||||
```
|
||||
- Added `select` and `deselect` action for unconditionally selecting or
|
||||
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
||||
- Sigificant performance improvement in ANSI code processing
|
||||
- Bug fixes and improvements
|
||||
- Built with Go 1.16
|
||||
|
||||
0.25.1
|
||||
------
|
||||
- Added `close` action
|
||||
|
6
Makefile
6
Makefile
@@ -66,12 +66,16 @@ endif
|
||||
all: target/$(BINARY)
|
||||
|
||||
test: $(SOURCES)
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||
github.com/junegunn/fzf/src \
|
||||
github.com/junegunn/fzf/src/algo \
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
build:
|
||||
@@ -152,4 +156,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all build release test install clean docker docker-test update
|
||||
.PHONY: all build release test bench install clean docker docker-test update
|
||||
|
@@ -25,7 +25,7 @@ Table of Contents
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
|
||||
* [Using Homebrew](#using-homebrew)
|
||||
* [Using git](#using-git)
|
||||
* [Using Linux package managers](#using-linux-package-managers)
|
||||
* [Windows](#windows)
|
||||
@@ -84,9 +84,9 @@ stuff.
|
||||
|
||||
[bin]: https://github.com/junegunn/fzf/releases
|
||||
|
||||
### Using Homebrew or Linuxbrew
|
||||
### Using Homebrew
|
||||
|
||||
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
|
||||
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
|
||||
to install fzf.
|
||||
|
||||
```sh
|
||||
@@ -411,7 +411,7 @@ unalias **<TAB>
|
||||
export FZF_COMPLETION_TRIGGER='~~'
|
||||
|
||||
# Options to fzf command
|
||||
export FZF_COMPLETION_OPTS='+c -x'
|
||||
export FZF_COMPLETION_OPTS='--border --info=inline'
|
||||
|
||||
# Use fd (https://github.com/sharkdp/fd) instead of the default find
|
||||
# command for listing path candidates.
|
||||
|
11
bin/fzf-tmux
11
bin/fzf-tmux
@@ -204,7 +204,16 @@ if [[ "$opt" =~ "-K -E" ]]; then
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
|
||||
|
||||
# tmux dropped the support for `-K`, `-R` to popup command
|
||||
# TODO: We can remove this once tmux 3.2 is released
|
||||
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
|
||||
opt="${opt/-K/}"
|
||||
else
|
||||
opt="${opt} -R"
|
||||
fi
|
||||
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
|
||||
exit $?
|
||||
fi
|
||||
|
||||
|
2
go.sum
2
go.sum
@@ -17,6 +17,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
@@ -32,4 +33,5 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
5
install
5
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.25.1
|
||||
version=0.26.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -168,7 +168,8 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
# Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;; # TODO
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.25.1"
|
||||
$version="0.26.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.25"
|
||||
var version string = "0.26"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Feb 2021" "fzf 0.25.1" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Mar 2021" "fzf 0.26.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
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
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Feb 2021" "fzf 0.25.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Mar 2021" "fzf 0.26.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -442,7 +442,7 @@ e.g.
|
||||
done'\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES][:default]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -480,12 +480,17 @@ e.g.
|
||||
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
|
||||
sharp edges), or \fBnoborder\fR (no border).
|
||||
|
||||
* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
|
||||
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
|
||||
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
|
||||
for adjusting the base offset so that you can see the text above it. It should
|
||||
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
||||
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
||||
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
|
||||
preview window.
|
||||
|
||||
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
|
||||
|
||||
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR).
|
||||
|
||||
- The final \fB/DENOM\fR part is for specifying a fraction of the preview window height.
|
||||
|
||||
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
|
||||
are always visible.
|
||||
|
||||
* \fBdefault\fR resets all options previously set to the default.
|
||||
|
||||
@@ -498,14 +503,23 @@ e.g.
|
||||
# Initial scroll offset is set to the line number of each line of
|
||||
# git grep output *minus* 5 lines (-5)
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
|
||||
fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5'
|
||||
|
||||
# Preview with bat, matching line in the middle of the window (-/2)
|
||||
# Preview with bat, matching line in the middle of the window below
|
||||
# the fixed header of the top 3 lines
|
||||
#
|
||||
# ~3 Top 3 lines as the fixed header
|
||||
# +{2} Base scroll offset extracted from the second field
|
||||
# +3 Extra offset to compensate for the 3-line header
|
||||
# /2 Put in the middle of the preview area
|
||||
#
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : \\
|
||||
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
||||
--preview-window +{2}-/2\fR
|
||||
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \\
|
||||
--preview-window '~3:+{2}+3/2'
|
||||
|
||||
# Display top 3 lines as the fixed header
|
||||
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
|
||||
.RE
|
||||
|
||||
.SS Scripting
|
||||
@@ -784,6 +798,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBclear-query\fR (clear query string)
|
||||
\fBdelete-char\fR \fIdel\fR
|
||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||
\fBdeselect\fR
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
@@ -819,6 +834,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBrefresh-preview\fR
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
|
@@ -764,6 +764,13 @@ function! s:split(dict)
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
nnoremap <silent> <Plug>(fzf-insert) i
|
||||
nnoremap <silent> <Plug>(fzf-normal) <Nop>
|
||||
if exists(':tnoremap')
|
||||
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
|
||||
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||
endif
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
@@ -776,7 +783,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
function! fzf.switch_back(inplace)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
if bufexists(self.pbuf)
|
||||
execute 'keepalt b' self.pbuf
|
||||
execute 'keepalt keepjumps b' self.pbuf
|
||||
endif
|
||||
" No other listed buffer
|
||||
if bufnr('') == self.buf
|
||||
@@ -792,8 +799,6 @@ function! s:execute_term(dict, command, temps) abort
|
||||
call self.switch_back(1)
|
||||
else
|
||||
if bufnr('') == self.buf
|
||||
" Exit terminal mode first (see neovim#13769)
|
||||
call feedkeys("\<C-\>\<C-n>", 'n')
|
||||
" We use close instead of bd! since Vim does not close the split when
|
||||
" there's no other listed buffer (nvim +'set nobuflisted')
|
||||
close
|
||||
@@ -818,6 +823,10 @@ function! s:execute_term(dict, command, temps) abort
|
||||
call s:pushd(self.dict)
|
||||
call s:callback(self.dict, lines)
|
||||
call self.switch_back(s:getpos() == self.ppos)
|
||||
|
||||
if &buftype == 'terminal'
|
||||
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
|
||||
endif
|
||||
endfunction
|
||||
|
||||
try
|
||||
@@ -833,7 +842,10 @@ function! s:execute_term(dict, command, temps) abort
|
||||
if has('nvim')
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'}
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
if v:version >= 802
|
||||
let term_opts.term_kill = 'term'
|
||||
endif
|
||||
if is_popup
|
||||
let term_opts.hidden = 1
|
||||
else
|
||||
@@ -906,13 +918,9 @@ if has('nvim')
|
||||
function s:create_popup(hl, opts) abort
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
if !empty(border)
|
||||
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
|
||||
endif
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
|
@@ -20,6 +20,7 @@ function fzf_key_bindings
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
@@ -42,6 +43,7 @@ function fzf_key_bindings
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
@@ -74,6 +76,7 @@ function fzf_key_bindings
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
@@ -88,6 +91,7 @@ function fzf_key_bindings
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,9 +120,15 @@ function fzf_key_bindings
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
||||
eval set commandline $commandline
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
@@ -138,6 +148,7 @@ function fzf_key_bindings
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
echo $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
|
263
src/ansi.go
263
src/ansi.go
@@ -1,8 +1,6 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -82,73 +80,154 @@ func toAnsiString(color tui.Color, offset int) string {
|
||||
return ret + ";"
|
||||
}
|
||||
|
||||
var ansiRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
/*
|
||||
References:
|
||||
- https://github.com/gnachman/iTerm2
|
||||
- http://ascii-table.com/ansi-escape-sequences.php
|
||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
func isPrint(c uint8) bool {
|
||||
return '\x20' <= c && c <= '\x7e'
|
||||
}
|
||||
|
||||
func findAnsiStart(str string) int {
|
||||
idx := 0
|
||||
for ; idx < len(str); idx++ {
|
||||
b := str[idx]
|
||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
||||
return idx
|
||||
func matchOperatingSystemCommand(s string) int {
|
||||
// `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
if b == 0x08 && idx > 0 {
|
||||
return idx - 1
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
return idx
|
||||
return -1
|
||||
}
|
||||
|
||||
func matchControlSequence(s string) int {
|
||||
// `\x1b[\\[()][0-9;]*[a-zA-Z@]`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';'); i++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
c := s[i]
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func isCtrlSeqStart(c uint8) bool {
|
||||
return c == '\\' || c == '[' || c == '(' || c == ')'
|
||||
}
|
||||
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||
//
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
goto Loop
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
if s[i-1] < utf8.RuneSelf {
|
||||
return i - 1, i + 1
|
||||
}
|
||||
_, n := utf8.DecodeLastRuneInString(s[:i])
|
||||
return i - n, i + 1
|
||||
}
|
||||
case '\x1b':
|
||||
// match: `\x1b[\\[()][0-9;]*[a-zA-Z@]`
|
||||
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
|
||||
if j := matchControlSequence(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
||||
s[i+3] == ';' && isPrint(s[i+4]) {
|
||||
|
||||
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b.`
|
||||
if i+1 < len(s) && s[i+1] != '\n' {
|
||||
if s[i+1] < utf8.RuneSelf {
|
||||
return i, i + 2
|
||||
}
|
||||
_, n := utf8.DecodeRuneInString(s[i+1:])
|
||||
return i, i + n + 1
|
||||
}
|
||||
case '\x0e', '\x0f':
|
||||
// match: `[\x0e\x0f]`
|
||||
return i, i + 1
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
var offsets []ansiOffset
|
||||
var output bytes.Buffer
|
||||
// We append to a stack allocated variable that we'll
|
||||
// later copy and return, to save on allocations.
|
||||
offsets := make([]ansiOffset, 0, 32)
|
||||
|
||||
if state != nil {
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
var (
|
||||
pstate *ansiState // lazily allocated
|
||||
output strings.Builder
|
||||
prevIdx int
|
||||
runeCount int
|
||||
)
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
if idx == len(str) {
|
||||
// Make sure that we found an ANSI code
|
||||
start, end := nextAnsiEscapeSequence(str[idx:])
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure that we found an ANSI code
|
||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
||||
if len(offset) < 2 {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
offset[0] += idx
|
||||
offset[1] += idx
|
||||
idx = offset[1]
|
||||
start += idx
|
||||
idx += end
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
prev := str[prevIdx:start]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
prevIdx = idx
|
||||
|
||||
prevIdx = offset[1]
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
output.WriteString(prev)
|
||||
if len(prev) != 0 {
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
// Grow the buffer size to the maximum possible length (string length
|
||||
// containing ansi codes) to avoid repetitive allocation
|
||||
if output.Cap() == 0 {
|
||||
output.Grow(len(str))
|
||||
}
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
newState := interpretCode(str[start:idx], state)
|
||||
if !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
@@ -157,8 +236,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
if pstate == nil {
|
||||
pstate = &ansiState{}
|
||||
}
|
||||
*pstate = newState
|
||||
state = pstate
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
newState,
|
||||
})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
@@ -168,7 +254,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
var rest string
|
||||
var trimmed string
|
||||
|
||||
if prevIdx == 0 {
|
||||
// No ANSI code found
|
||||
rest = str
|
||||
@@ -178,51 +263,75 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(rest)
|
||||
trimmed = output.String()
|
||||
}
|
||||
if len(rest) > 0 && state != nil {
|
||||
// Update last offset
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
if proc != nil {
|
||||
proc(rest, state)
|
||||
}
|
||||
if len(offsets) == 0 {
|
||||
return trimmed, nil, state
|
||||
if len(offsets) > 0 {
|
||||
if len(rest) > 0 && state != nil {
|
||||
// Update last offset
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
// Return a copy of the offsets slice
|
||||
a := make([]ansiOffset, len(offsets))
|
||||
copy(a, offsets)
|
||||
return trimmed, &a, state
|
||||
}
|
||||
return trimmed, &offsets, state
|
||||
return trimmed, nil, state
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
// State
|
||||
var state *ansiState
|
||||
func parseAnsiCode(s string) (int, string) {
|
||||
var remaining string
|
||||
if i := strings.IndexByte(s, ';'); i >= 0 {
|
||||
remaining = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
// Inlined version of strconv.Atoi() that only handles positive
|
||||
// integers and does not allocate on error.
|
||||
code := 0
|
||||
for _, ch := range []byte(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, remaining
|
||||
}
|
||||
code = code*10 + int(ch)
|
||||
}
|
||||
return code, remaining
|
||||
}
|
||||
|
||||
return -1, remaining
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = &ansiState{-1, -1, 0, -1}
|
||||
state = ansiState{-1, -1, 0, -1}
|
||||
} else {
|
||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if strings.HasSuffix(ansiCode, "0K") && prevState != nil {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
state.lbg = prevState.bg
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
ptr := &state.fg
|
||||
state256 := 0
|
||||
|
||||
init := func() {
|
||||
if len(ansiCode) <= 3 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
return state
|
||||
}
|
||||
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
if len(ansiCode) == 0 {
|
||||
init()
|
||||
}
|
||||
for _, code := range strings.Split(ansiCode, ";") {
|
||||
if num, err := strconv.Atoi(code); err == nil {
|
||||
|
||||
state256 := 0
|
||||
ptr := &state.fg
|
||||
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -253,7 +362,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 0:
|
||||
init()
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
state.fg = tui.Color(num - 30)
|
||||
@@ -289,6 +401,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
239
src/ansi_test.go
239
src/ansi_test.go
@@ -2,12 +2,190 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences. This regex is used as a reference for
|
||||
// testing nextAnsiEscapeSequence().
|
||||
//
|
||||
// References:
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - http://ascii-table.com/ansi-escape-sequences.php
|
||||
// - http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexRefence = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
|
||||
toSlice := func(start, end int) []int {
|
||||
if start == -1 {
|
||||
return nil
|
||||
}
|
||||
return []int{start, end}
|
||||
}
|
||||
|
||||
s := str
|
||||
for i := 0; ; i++ {
|
||||
got := toSlice(nextAnsiEscapeSequence(s))
|
||||
exp := ansiRegexRefence.FindStringIndex(s)
|
||||
|
||||
equal := len(got) == len(exp)
|
||||
if equal {
|
||||
for i := 0; i < len(got); i++ {
|
||||
if got[i] != exp[i] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !equal {
|
||||
var exps, gots []rune
|
||||
if len(got) == 2 {
|
||||
gots = []rune(s[got[0]:got[1]])
|
||||
}
|
||||
if len(exp) == 2 {
|
||||
exps = []rune(s[exp[0]:exp[1]])
|
||||
}
|
||||
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
|
||||
return
|
||||
}
|
||||
if len(exp) == 0 {
|
||||
return
|
||||
}
|
||||
s = s[exp[1]:]
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence(t *testing.T) {
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
"\x1b椙",
|
||||
"椙\x08",
|
||||
"\n\x08",
|
||||
"X\x08",
|
||||
"",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
for _, s := range testStrs {
|
||||
testParserReference(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
|
||||
|
||||
modifyString := func(s string, rr *rand.Rand) string {
|
||||
n := rr.Intn(len(s))
|
||||
b := []rune(s)
|
||||
for ; n >= 0 && len(b) != 0; n-- {
|
||||
i := rr.Intn(len(b))
|
||||
switch x := rr.Intn(4); x {
|
||||
case 0:
|
||||
b = append(b[:i], b[i+1:]...)
|
||||
case 1:
|
||||
j := rr.Intn(len(replacementBytes) - 1)
|
||||
b[i] = replacementBytes[j]
|
||||
case 2:
|
||||
x := rune(rr.Intn(utf8.MaxRune))
|
||||
for !utf8.ValidRune(x) {
|
||||
x = rune(rr.Intn(utf8.MaxRune))
|
||||
}
|
||||
b[i] = x
|
||||
case 3:
|
||||
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
|
||||
default:
|
||||
t.Fatalf("unsupported value: %d", x)
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for _, s := range testStrs {
|
||||
for i := 1_000; i >= 0; i-- {
|
||||
testParserReference(t, modifyString(s, rr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
randomString := func(rr *rand.Rand) string {
|
||||
numChars := rand.Intn(50)
|
||||
codePoints := make([]rune, numChars)
|
||||
for i := 0; i < len(codePoints); i++ {
|
||||
var r rune
|
||||
for n := 0; n < 1000; n++ {
|
||||
r = rune(rr.Intn(utf8.MaxRune))
|
||||
// Allow 10% of runes to be invalid
|
||||
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
codePoints[i] = r
|
||||
}
|
||||
return string(codePoints)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100_000; i++ {
|
||||
testParserReference(t, randomString(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractColor(t *testing.T) {
|
||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||
var attr tui.Attr
|
||||
@@ -185,3 +363,64 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||
}
|
||||
|
||||
func TestParseAnsiCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
In, Exp string
|
||||
N int
|
||||
}{
|
||||
{"123", "", 123},
|
||||
{"1a", "", -1},
|
||||
{"1a;12", "12", -1},
|
||||
{"12;a", "a", 12},
|
||||
{"-2", "", -1},
|
||||
}
|
||||
for _, x := range tests {
|
||||
n, s := parseAnsiCode(x.In)
|
||||
if n != x.N || s != x.Exp {
|
||||
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kernel/bpf/preload/iterators/README
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
|
||||
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||
|
||||
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
_, o := nextAnsiEscapeSequence(s)
|
||||
if o == -1 {
|
||||
break
|
||||
}
|
||||
s = s[o:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
|
||||
// previously used regex based implementation.
|
||||
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
a := ansiRegexRefence.FindStringIndex(s)
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
s = s[a[1]:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtractColor(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
extractColor(ansiBenchmarkString, nil, nil)
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
|
||||
cache := NewChunkCache()
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &Chunk{count: chunkSize}
|
||||
items1 := []Result{Result{}}
|
||||
items2 := []Result{Result{}, Result{}}
|
||||
items1 := []Result{{}}
|
||||
items2 := []Result{{}, {}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
@@ -73,6 +73,7 @@ const (
|
||||
EvtSearchFin
|
||||
EvtHeader
|
||||
EvtReady
|
||||
EvtQuit
|
||||
)
|
||||
|
||||
const (
|
||||
|
@@ -254,7 +254,11 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
for evt, value := range *events {
|
||||
switch evt {
|
||||
|
||||
case EvtQuit:
|
||||
if reading {
|
||||
reader.terminate()
|
||||
}
|
||||
os.Exit(value.(int))
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
|
@@ -106,7 +106,6 @@ func (mg *Merger) mergedGet(idx int) Result {
|
||||
minIdx = listIdx
|
||||
}
|
||||
}
|
||||
mg.cursors[listIdx] = cursor
|
||||
}
|
||||
|
||||
if minIdx >= 0 {
|
||||
|
@@ -84,7 +84,7 @@ const usage = `usage: fzf [options]
|
||||
[up|down|left|right][:SIZE[%]]
|
||||
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
|
||||
[:rounded|sharp|noborder]
|
||||
[:+SCROLL[-OFFSET]]
|
||||
[:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES]
|
||||
[:default]
|
||||
|
||||
Scripting
|
||||
@@ -161,15 +161,16 @@ const (
|
||||
)
|
||||
|
||||
type previewOpts struct {
|
||||
command string
|
||||
position windowPosition
|
||||
size sizeSpec
|
||||
scroll string
|
||||
hidden bool
|
||||
wrap bool
|
||||
cycle bool
|
||||
follow bool
|
||||
border tui.BorderShape
|
||||
command string
|
||||
position windowPosition
|
||||
size sizeSpec
|
||||
scroll string
|
||||
hidden bool
|
||||
wrap bool
|
||||
cycle bool
|
||||
follow bool
|
||||
border tui.BorderShape
|
||||
headerLines int
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
@@ -231,7 +232,7 @@ type Options struct {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -839,6 +840,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
appendAction(actEndOfLine)
|
||||
case "cancel":
|
||||
@@ -879,6 +882,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
appendAction(actSelectAll)
|
||||
case "deselect-all":
|
||||
@@ -1073,7 +1078,8 @@ func parseInfoStyle(str string) infoStyle {
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
tokens := strings.Split(input, ":")
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case "":
|
||||
@@ -1110,10 +1116,12 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
case "nofollow":
|
||||
opts.follow = false
|
||||
default:
|
||||
if sizeRegex.MatchString(token) {
|
||||
if headerRegex.MatchString(token) {
|
||||
opts.headerLines = atoi(token[1:])
|
||||
} else if sizeRegex.MatchString(token) {
|
||||
opts.size = parseSize(token, 99, "window size")
|
||||
} else if offsetRegex.MatchString(token) {
|
||||
opts.scroll = token[1:]
|
||||
opts.scroll = token
|
||||
} else {
|
||||
errorExit("invalid preview window option: " + token)
|
||||
}
|
||||
@@ -1360,7 +1368,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Preview.command = ""
|
||||
case "--preview-window":
|
||||
parsePreviewWindow(&opts.Preview,
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]"))
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES][:default]"))
|
||||
case "--height":
|
||||
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
||||
case "--min-height":
|
||||
|
@@ -102,7 +102,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
}
|
||||
}
|
||||
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
@@ -389,18 +389,18 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posLeft &&
|
||||
opts.Preview.scroll == "{1}-/2" &&
|
||||
opts.Preview.scroll == "+{1}-/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle")
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.cycle == true &&
|
||||
opts.Preview.position == posDown &&
|
||||
opts.Preview.scroll == "{1}-/2" &&
|
||||
opts.Preview.scroll == "+{1}+3-1-2/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview.size.size)
|
||||
|
@@ -337,7 +337,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
|
@@ -131,7 +131,7 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
trans := Transform(tokens, []Range{{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
|
@@ -18,8 +18,8 @@ func withIndex(i *Item, index int) *Item {
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
{3, 5}, {2, 7},
|
||||
{1, 3}, {2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
|
||||
item4 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
|
||||
item5 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
|
||||
item6 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
|
||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if !(items[0] == item6 && items[1] == item5 &&
|
||||
|
184
src/terminal.go
184
src/terminal.go
@@ -22,8 +22,9 @@ import (
|
||||
// import "github.com/pkg/profile"
|
||||
|
||||
var placeholder *regexp.Regexp
|
||||
var numericPrefix *regexp.Regexp
|
||||
var whiteSuffix *regexp.Regexp
|
||||
var offsetComponentRegex *regexp.Regexp
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var activeTempFiles []string
|
||||
|
||||
const ellipsis string = ".."
|
||||
@@ -31,8 +32,9 @@ const clearCode string = "\x1b[2J"
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
||||
numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||
activeTempFiles = []string{}
|
||||
}
|
||||
|
||||
@@ -133,6 +135,7 @@ type Terminal struct {
|
||||
count int
|
||||
progress int
|
||||
reading bool
|
||||
running bool
|
||||
failed *string
|
||||
jumping jumpMode
|
||||
jumpLabels string
|
||||
@@ -157,6 +160,7 @@ type Terminal struct {
|
||||
slab *util.Slab
|
||||
theme *tui.ColorTheme
|
||||
tui tui.Renderer
|
||||
executing *util.AtomicBool
|
||||
}
|
||||
|
||||
type selectedItem struct {
|
||||
@@ -276,6 +280,8 @@ const (
|
||||
actReload
|
||||
actDisableSearch
|
||||
actEnableSearch
|
||||
actSelect
|
||||
actDeselect
|
||||
)
|
||||
|
||||
type placeholderFlags struct {
|
||||
@@ -502,6 +508,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
reading: true,
|
||||
running: true,
|
||||
failed: nil,
|
||||
jumping: jumpDisabled,
|
||||
jumpLabels: opts.JumpLabels,
|
||||
@@ -523,7 +530,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
startChan: make(chan bool, 1),
|
||||
killChan: make(chan int),
|
||||
tui: renderer,
|
||||
initFunc: func() { renderer.Init() }}
|
||||
initFunc: func() { renderer.Init() },
|
||||
executing: util.NewAtomicBool(false)}
|
||||
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
||||
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
||||
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
||||
@@ -1140,8 +1148,8 @@ func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
|
||||
width = util.Max(0, width)
|
||||
var trimmed int32
|
||||
// Assume that each rune takes at least one column on screen
|
||||
if len(runes) > width {
|
||||
diff := len(runes) - width
|
||||
if len(runes) > width+2 {
|
||||
diff := len(runes) - width - 2
|
||||
trimmed = int32(diff)
|
||||
runes = runes[diff:]
|
||||
}
|
||||
@@ -1289,18 +1297,37 @@ func (t *Terminal) renderPreviewSpinner() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
lineNo := -t.previewer.offset
|
||||
height := t.pwindow.Height()
|
||||
func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
if unchanged {
|
||||
t.pwindow.MoveAndClear(0, 0)
|
||||
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
|
||||
} else {
|
||||
t.previewed.filled = false
|
||||
t.pwindow.Erase()
|
||||
}
|
||||
|
||||
height := t.pwindow.Height()
|
||||
header := []string{}
|
||||
body := t.previewer.lines
|
||||
headerLines := t.previewOpts.headerLines
|
||||
// Do not enable preview header lines if it's value is too large
|
||||
if headerLines > 0 && headerLines < util.Min(len(body), height) {
|
||||
header = t.previewer.lines[0:headerLines]
|
||||
body = t.previewer.lines[headerLines:]
|
||||
// Always redraw header
|
||||
t.renderPreviewText(height, header, 0, false)
|
||||
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||
}
|
||||
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
|
||||
|
||||
if !unchanged {
|
||||
t.pwindow.FinishFill()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
var ansi *ansiState
|
||||
for _, line := range t.previewer.lines {
|
||||
for _, line := range lines {
|
||||
var lbg tui.Color = -1
|
||||
if ansi != nil {
|
||||
ansi.lbg = -1
|
||||
@@ -1314,8 +1341,9 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
prefixWidth := 0
|
||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||
trimmed := []rune(str)
|
||||
trimmedLen := 0
|
||||
if !t.previewOpts.wrap {
|
||||
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
trimmed, trimmedLen = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
prefixWidth += width
|
||||
@@ -1325,7 +1353,8 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
} else {
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||
}
|
||||
return fillRet == tui.FillContinue
|
||||
return trimmedLen == 0 &&
|
||||
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||
})
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||
if fillRet == tui.FillNextLine {
|
||||
@@ -1346,9 +1375,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
}
|
||||
lineNo++
|
||||
}
|
||||
if !unchanged {
|
||||
t.pwindow.FinishFill()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printPreview() {
|
||||
@@ -1361,7 +1387,7 @@ func (t *Terminal) printPreview() {
|
||||
t.previewer.version == t.previewed.version &&
|
||||
t.previewer.offset == t.previewed.offset
|
||||
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||
t.renderPreviewText(unchanged)
|
||||
t.renderPreviewArea(unchanged)
|
||||
t.renderPreviewSpinner()
|
||||
t.previewed.numLines = numLines
|
||||
t.previewed.version = t.previewer.version
|
||||
@@ -1374,7 +1400,7 @@ func (t *Terminal) printPreviewDelayed() {
|
||||
}
|
||||
|
||||
t.previewer.scrollable = false
|
||||
t.renderPreviewText(true)
|
||||
t.renderPreviewArea(true)
|
||||
|
||||
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
||||
pos := t.pwindow.Width() - len(message)
|
||||
@@ -1567,43 +1593,33 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
|
||||
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
||||
}
|
||||
|
||||
// Ascii to positive integer
|
||||
func atopi(s string) int {
|
||||
matches := numericPrefix.FindStringSubmatch(s)
|
||||
if len(matches) < 2 {
|
||||
return 0
|
||||
}
|
||||
n, e := strconv.Atoi(matches[1])
|
||||
if e != nil || n < 1 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
|
||||
offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list)
|
||||
nums := strings.Split(offsetExpr, "-")
|
||||
switch len(nums) {
|
||||
case 0:
|
||||
return 0
|
||||
case 1, 2:
|
||||
base := atopi(nums[0])
|
||||
if base == 0 {
|
||||
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
|
||||
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
|
||||
|
||||
atoi := func(s string) int {
|
||||
n, e := strconv.Atoi(s)
|
||||
if e != nil {
|
||||
return 0
|
||||
} else if len(nums) == 1 {
|
||||
return base - 1
|
||||
}
|
||||
if nums[1][0] == '/' {
|
||||
denom := atopi(nums[1][1:])
|
||||
return n
|
||||
}
|
||||
|
||||
base := -1
|
||||
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
|
||||
if strings.HasPrefix(component, "-/") {
|
||||
component = component[1:]
|
||||
}
|
||||
if component[0] == '/' {
|
||||
denom := atoi(component[1:])
|
||||
if denom == 0 {
|
||||
return base
|
||||
}
|
||||
return base - height/denom
|
||||
}
|
||||
return base - atopi(nums[1]) - 1
|
||||
default:
|
||||
return 0
|
||||
base += atoi(component)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
@@ -1711,13 +1727,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
t.tui.Pause(true)
|
||||
t.executing.Set(true)
|
||||
cmd.Run()
|
||||
t.executing.Set(false)
|
||||
t.tui.Resume(true, false)
|
||||
t.redraw()
|
||||
t.refresh()
|
||||
} else {
|
||||
t.tui.Pause(false)
|
||||
t.executing.Set(true)
|
||||
cmd.Run()
|
||||
t.executing.Set(false)
|
||||
t.tui.Resume(false, false)
|
||||
}
|
||||
cleanTemporaryFiles()
|
||||
@@ -1785,11 +1805,26 @@ func (t *Terminal) selectItem(item *Item) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Terminal) selectItemChanged(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; found {
|
||||
return false
|
||||
}
|
||||
return t.selectItem(item)
|
||||
}
|
||||
|
||||
func (t *Terminal) deselectItem(item *Item) {
|
||||
delete(t.selected, item.Index())
|
||||
t.version++
|
||||
}
|
||||
|
||||
func (t *Terminal) deselectItemChanged(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; found {
|
||||
t.deselectItem(item)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Terminal) toggleItem(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; !found {
|
||||
return t.selectItem(item)
|
||||
@@ -1803,7 +1838,7 @@ func (t *Terminal) killPreview(code int) {
|
||||
case t.killChan <- code:
|
||||
default:
|
||||
if code != exitCancel {
|
||||
os.Exit(code)
|
||||
t.eventBox.Set(EvtQuit, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1820,8 +1855,12 @@ func (t *Terminal) Loop() {
|
||||
intChan := make(chan os.Signal, 1)
|
||||
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-intChan
|
||||
t.reqBox.Set(reqQuit, nil)
|
||||
for s := range intChan {
|
||||
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
|
||||
if !(s == os.Interrupt && t.executing.Get()) {
|
||||
t.reqBox.Set(reqQuit, nil)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
contChan := make(chan os.Signal, 1)
|
||||
@@ -1898,7 +1937,7 @@ func (t *Terminal) Loop() {
|
||||
cmd := util.ExecCommand(command, true)
|
||||
if pwindow != nil {
|
||||
height := pwindow.Height()
|
||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
|
||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
|
||||
env := os.Environ()
|
||||
lines := fmt.Sprintf("LINES=%d", height)
|
||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||
@@ -1983,7 +2022,7 @@ func (t *Terminal) Loop() {
|
||||
case code := <-t.killChan:
|
||||
if code != exitCancel {
|
||||
util.KillCommand(cmd)
|
||||
os.Exit(code)
|
||||
t.eventBox.Set(EvtQuit, code)
|
||||
} else {
|
||||
timer := time.NewTimer(previewCancelWait)
|
||||
select {
|
||||
@@ -2020,16 +2059,6 @@ func (t *Terminal) Loop() {
|
||||
}()
|
||||
}
|
||||
|
||||
exit := func(getCode func() int) {
|
||||
t.tui.Close()
|
||||
code := getCode()
|
||||
if code <= exitNoMatch && t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
// prof.Stop()
|
||||
t.killPreview(code)
|
||||
}
|
||||
|
||||
refreshPreview := func(command string) {
|
||||
if len(command) > 0 && t.isPreviewEnabled() {
|
||||
_, list := t.buildPlusList(command, false)
|
||||
@@ -2041,7 +2070,19 @@ func (t *Terminal) Loop() {
|
||||
go func() {
|
||||
var focusedIndex int32 = minItem.Index()
|
||||
var version int64 = -1
|
||||
for {
|
||||
running := true
|
||||
code := exitError
|
||||
exit := func(getCode func() int) {
|
||||
t.tui.Close()
|
||||
code = getCode()
|
||||
if code <= exitNoMatch && t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
running = false
|
||||
t.mutex.Unlock()
|
||||
}
|
||||
|
||||
for running {
|
||||
t.reqBox.Wait(func(events *util.Events) {
|
||||
defer events.Clear()
|
||||
t.mutex.Lock()
|
||||
@@ -2087,6 +2128,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
return exitNoMatch
|
||||
})
|
||||
return
|
||||
case reqPreviewDisplay:
|
||||
result := value.(previewResult)
|
||||
if t.previewer.version != result.version {
|
||||
@@ -2098,7 +2140,7 @@ func (t *Terminal) Loop() {
|
||||
if t.previewer.following {
|
||||
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
||||
} else if result.offset >= 0 {
|
||||
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
|
||||
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
|
||||
}
|
||||
t.printPreview()
|
||||
case reqPreviewRefresh:
|
||||
@@ -2111,14 +2153,18 @@ func (t *Terminal) Loop() {
|
||||
t.printer(string(t.input))
|
||||
return exitOk
|
||||
})
|
||||
return
|
||||
case reqQuit:
|
||||
exit(func() int { return exitInterrupt })
|
||||
return
|
||||
}
|
||||
}
|
||||
t.refresh()
|
||||
t.mutex.Unlock()
|
||||
})
|
||||
}
|
||||
// prof.Stop()
|
||||
t.killPreview(code)
|
||||
}()
|
||||
|
||||
looping := true
|
||||
@@ -2167,7 +2213,7 @@ func (t *Terminal) Loop() {
|
||||
if t.previewOpts.cycle {
|
||||
newOffset = (newOffset + numLines) % numLines
|
||||
}
|
||||
newOffset = util.Constrain(newOffset, 0, numLines-1)
|
||||
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
|
||||
if t.previewer.offset != newOffset {
|
||||
t.previewer.offset = newOffset
|
||||
req(reqPreviewRefresh)
|
||||
@@ -2341,6 +2387,16 @@ func (t *Terminal) Loop() {
|
||||
} else {
|
||||
req(reqQuit)
|
||||
}
|
||||
case actSelect:
|
||||
current := t.currentItem()
|
||||
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
|
||||
req(reqList, reqInfo)
|
||||
}
|
||||
case actDeselect:
|
||||
current := t.currentItem()
|
||||
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
|
||||
req(reqList, reqInfo)
|
||||
}
|
||||
case actToggle:
|
||||
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||
req(reqList)
|
||||
|
@@ -906,12 +906,6 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.Move(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
@@ -926,6 +920,14 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.posx+1 >= w.Width() {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
|
154
test/test_go.rb
154
test/test_go.rb
@@ -1890,6 +1890,158 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
end
|
||||
|
||||
def test_select_deselect
|
||||
tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 0, lines.select_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
end
|
||||
|
||||
def test_interrupt_execute
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'C-l'
|
||||
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 99
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_abort
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', readonce.chomp
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', readonce.chomp
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '4/1000'
|
||||
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||
end
|
||||
tmux.send_keys '55'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-J'
|
||||
tmux.until do |lines|
|
||||
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 19, lines.match_count
|
||||
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-K'
|
||||
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2060,7 +2212,7 @@ module CompletionTest
|
||||
end
|
||||
|
||||
# ~USERNAME**<TAB>
|
||||
user = ENV['USER']
|
||||
user = `whoami`.chomp
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~#{user}**", :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
|
Reference in New Issue
Block a user