m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-16 07:13:48 -05:00

Compare commits

...

55 Commits

Author SHA1 Message Date
Junegunn Choi
ecb6b234cc 0.16.11 2017-08-02 02:50:28 +09:00
Junegunn Choi
39dbc8acdb Exit 2 instead of panic when failed to open /dev/tty 2017-08-02 02:50:26 +09:00
Junegunn Choi
a56489bc7f Remove non-exclusive access to ChunkList field 2017-08-02 00:09:00 +09:00
Junegunn Choi
99927c7071 Modify loop conditions in checkAscii function 2017-08-01 22:04:42 +09:00
Junegunn Choi
3e28403978 [man] Add note on --no- convention
Close #1003
2017-08-01 21:34:44 +09:00
Junegunn Choi
37370f057f Do not use defer in performance-sensitive contexts 2017-08-01 03:44:55 +09:00
Junegunn Choi
f4b46fad27 Inline function calls in a tight loop
Manually inline function calls in a tight loop as Go compiler does not
inline non-leaf functions. It is observed that this unpleasant code
change resulted up to 10% performance improvement.
2017-08-01 03:44:38 +09:00
Junegunn Choi
9d2c6a95f4 Revert "[bash] Do not append space when path completion is cancelled"
This reverts commit 376a76d1d3 as it
affects normal completion
2017-07-31 14:08:17 +09:00
Junegunn Choi
376a76d1d3 [bash] Do not append space when path completion is cancelled
Close #990
2017-07-30 21:51:44 +09:00
Jan Edmund Lazo
1fcc07e54e [vim] Fix escape of backslash in s:shortpath
Close #1000
2017-07-30 20:05:01 +09:00
Junegunn Choi
8db3345c2f Optimize exact match by applying the same trick for fuzzy match 2017-07-30 18:16:54 +09:00
Junegunn Choi
69aa2fea68 Optimize fuzzy search performance for ASCII strings 2017-07-30 17:31:50 +09:00
Junegunn Choi
298749bfcd Update README 2017-07-29 17:12:46 +09:00
Junegunn Choi
f1f31baae1 Update README: Missing TOC 2017-07-29 17:10:00 +09:00
Junegunn Choi
e1c8f19e8f Update README: Advanced topics 2017-07-29 17:09:05 +09:00
Junegunn Choi
5e302c70e9 Update README: rg intead of pt 2017-07-29 17:09:05 +09:00
Junegunn Choi
4c5a679066 Make deselect-all instantaneous 2017-07-28 13:13:03 +09:00
Andrew Halberstadt
41f0b2c354 Add MinGW on Windows to install script (#998)
Running uname -sm yields:
MINGW32_NT-6.2 i686
2017-07-28 12:22:33 +09:00
Junegunn Choi
a0a3c349c9 Update preview window when selection has changed
Close #995
2017-07-28 01:39:25 +09:00
Alexey Shamrin
bc3983181d Update fish comments, because 2.6.0 was released (#991) 2017-07-25 19:10:34 +09:00
Junegunn Choi
980b58ef5a Update README
Removed outdated animated GIF.
2017-07-23 22:07:24 +09:00
Junegunn Choi
a2604c0963 [nvim] Disable number in fzf buffer
https://github.com/junegunn/fzf.vim/issues/396#issuecomment-317214036

One can override the setting on FileType fzf autocmd.
2017-07-23 13:12:15 +09:00
Junegunn Choi
6dbc108da2 0.16.10 2017-07-21 18:41:11 +09:00
Junegunn Choi
bd98f988f0 Further reduce unnecessary rune array conversion
I was too quick to release 0.16.9, this commit makes --ansi processing
even faster.
2017-07-21 17:31:11 +09:00
Junegunn Choi
06301c7847 Fix regression: ANSI color in preview window not cleared 2017-07-21 16:44:59 +09:00
Junegunn Choi
18a1aeaa91 0.16.9 2017-07-21 00:08:55 +09:00
Junegunn Choi
c9f16b6430 Avoid unconditionally storsing input as runes
When --with-nth is used, fzf used to preprocess each line and store the
result as rune array, which was wasteful if the line only contains ascii
characters.
2017-07-20 02:44:30 +09:00
Junegunn Choi
bc9d2abdb6 Improve preview window rendering
- Fix incorrect display of the last line when more than a line is
  wrapped above
- Avoid unnecessary flickering of the window
2017-07-19 22:47:15 +09:00
Junegunn Choi
28810c178f Optimize ANSI code scanner
This change gives 5x speed improvement
2017-07-19 21:49:41 +09:00
Junegunn Choi
a9e64efe45 Fix regression: output printed on alternate screen 2017-07-19 13:17:06 +09:00
Junegunn Choi
6b5886c034 Adjust --no-clear option for repetitive relaunching
Related: https://gist.github.com/junegunn/4963bab6ace453f7f529d2d0e01b1d85

Close #974
2017-07-18 21:10:49 +09:00
Junegunn Choi
7727ad43af [vim] Use fnameescape to escape command line arguments
Fix https://github.com/junegunn/fzf.vim/issues/404

Thanks to @janlazo.
2017-07-18 16:33:58 +09:00
Junegunn Choi
bbe10f4f77 Consolidate Result and rank structs
By not storing item index twice, we can cut down the size of Result
struct and now it makes more sense to store and pass Results by values.
Benchmarks show no degradation of performance by additional pointer
indirection for looking up index.
2017-07-18 03:14:33 +09:00
Junegunn Choi
5e72709613 Speed up initial scanning with bitwise AND operation 2017-07-18 02:17:05 +09:00
Junegunn Choi
9e85cba0d0 Reduce memory footprint of Item struct 2017-07-16 23:34:32 +09:00
Junegunn Choi
4b59ced08f Add gopath to gitignore 2017-07-16 23:34:32 +09:00
Junegunn Choi
8dbdd55730 Refactor cache lookup
- Remove multiple mutex locks in partial cache lookup
- Simplify return values
2017-07-16 23:34:32 +09:00
Junegunn Choi
6725151a99 Remove unnecessary copy of Chunk slice 2017-07-16 23:34:32 +09:00
Junegunn Choi
d4f3d5a164 Remove pointer indirection by changing Chunk definition 2017-07-16 23:34:32 +09:00
Tom Fitzhenry
7b5ccc45bc [fish] Fix ctrl-r regression in versions <2.4 (#972)
Close #966
2017-07-15 18:50:23 +09:00
Jan Edmund Lazo
940214a1a2 [neovim] Fix lcd when fzf job exits on Windows (#970)
Related: #960 (relative filepaths)
2017-07-10 02:06:13 +09:00
Jan Edmund Lazo
68bd410159 [vim] Don't pipe FZF_DEFAULT_COMMAND in Windows (#969)
Related #960, #552
2017-07-09 13:08:16 +09:00
Junegunn Choi
b13fcfd831 Add missing --no-expect flag 2017-07-04 23:02:15 +09:00
Junegunn Choi
07ef2b051c Print [ERROR] on info line when the default command failed
With zero result.

Related: https://github.com/junegunn/fzf.vim/issues/22#issuecomment-311869805
2017-07-01 01:13:15 +09:00
Junegunn Choi
3fc795340d Fix test failulre with non-zero pane-base-index 2017-07-01 01:05:47 +09:00
John Nguyen
70cfa6af13 [fish] Accept starting dir for <M-c> key binding (#944)
This also modifies <C-t> behaviour.
The longest file path in the input is used as root directory for `find`
command. The remainder of the input is passed to fzf's --query as a
initial search parameters.
2017-06-25 21:16:15 +09:00
Tom Fitzhenry
dbcaec59ae [fish] Support multiline commands (#954)
Fix found by @amosbird at https://github.com/junegunn/fzf/issues/953#issuecomment-310309055

closes #440
2017-06-25 21:09:51 +09:00
Junegunn Choi
faedae708e Fix FZF_CTRL_T_COMMAND example for fish
See #944
2017-06-23 01:50:45 +09:00
Junegunn Choi
0c66521b23 Fix handling of bracketed paste mode
fzf should immediately continue consuming the buffer after discarding
bracketed paste mode sequence.

Close #951
2017-06-22 02:35:57 +09:00
Junegunn Choi
bf92862459 Update man page: missing name "border" for --color 2017-06-20 14:15:11 +09:00
John Nguyen
1a68698d76 [fish] Fix <C-t> completion for current dir search (#946)
If "." is given as the argument to begin <C-t> completion, the leading
"." is not correctly removed. In general, if user selects a fzf
completion, the current token should be "consumed".
2017-06-12 18:24:45 +09:00
Junegunn Choi
842a73357c [fish] Fix CTRL-T with paths that don't start with ./
Close #943
2017-06-10 13:35:24 +09:00
Junegunn Choi
5efdeccdbb [vim] Expand 'dir' on Cygwin to handle Windows-style paths
See https://github.com/junegunn/fzf/pull/933#discussion_r120011934

Close https://github.com/junegunn/fzf.vim/pull/386
2017-06-09 12:00:59 +09:00
Jan Edmund Lazo
050777b8c4 [vim] Uncomment test case to escape % in cmd.exe (#941) 2017-06-08 10:25:35 +09:00
Uri Gorelik
a4d78e2200 Update CHANGELOG with unix-line-discard+top (#940)
Also change the example binding for `unix-word-rubout` to *ctrl-w* instead of *ctrl-u*
2017-06-08 10:02:34 +09:00
40 changed files with 846 additions and 521 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ Gemfile.lock
.DS_Store
doc/tags
vendor
gopath

View File

@@ -1,13 +1,35 @@
CHANGELOG
=========
0.16.11
-------
- Performance optimization
- Fixed missing preview update
0.16.10
-------
- Fixed invalid handling of ANSI colors in preview window
- Further improved `--ansi` performance
0.16.9
------
- Memory and performance optimization
- Around 20% performance improvement for general use cases
- Up to 5x faster processing of `--ansi`
- Up to 50% reduction of memory usage
- Bug fixes and usability improvements
- Fixed handling of bracketed paste mode
- [ERROR] on info line when the default command failed
- More efficient rendering of preview window
- `--no-clear` updated for repetitive relaunching scenarios
0.16.8
------
- New `change` event and `top` action for `--bind`
- `fzf --bind change:top`
- Move cursor to the top result whenever the query string is changed
- `fzf --bind ctrl-u:unix-word-rubout+top`
- `top` combined with `unix-word-rubout`
- `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'`
- `top` combined with `unix-word-rubout` and `unix-line-discard`
- Fixed inconsistent tiebreak scores when `--nth` is used
- Proper display of tab characters in `--prompt`
- Fixed not to `--cycle` on page-up/page-down to prevent overshoot

107
README.md
View File

@@ -3,15 +3,19 @@
fzf is a general-purpose command-line fuzzy finder.
![](https://raw.github.com/junegunn/i/master/fzf.gif)
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-preview.png" width=640>
It's an interactive Unix filter for command-line that can be used with any
list; files, command history, processes, hostnames, bookmarks, git commits,
etc.
Pros
----
- No dependencies
- Portable, no dependencies
- Blazingly fast
- The most comprehensive feature set
- Flexible layout using tmux panes
- Flexible layout
- Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
@@ -42,6 +46,10 @@ Table of Contents
* [Settings](#settings)
* [Supported commands](#supported-commands)
* [Vim plugin](#vim-plugin)
* [Advanced topics](#advanced-topics)
* [Performance](#performance)
* [Executing external programs](#executing-external-programs)
* [Preview window](#preview-window)
* [Tips](#tips)
* [Respecting .gitignore, <code>.hgignore</code>, and <code>svn:ignore</code>](#respecting-gitignore-hgignore-and-svnignore)
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
@@ -383,13 +391,94 @@ Vim plugin
See [README-VIM.md](README-VIM.md).
Advanced topics
---------------
### Performance
fzf is fast, and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the
options that affect the performance.
- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it
makes the initial scanning slower. So it's not recommended that you add it
to your `$FZF_DEFAULT_OPTS`.
- `--nth` makes fzf slower as fzf has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line.
- If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended.
[perf]: https://junegunn.kr/images/fzf-0.16.10.png
### Executing external programs
You can set up key bindings for starting external processes without leaving
fzf (`execute`, `execute-silent`).
```bash
# Press F1 to open the file with less without leaving fzf
# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy)
fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
```
See *KEY BINDINGS* section of the man page for details.
### Preview window
When `--preview` option is set, fzf automatically starts external process with
the current line as the argument and shows the result in the split window.
```bash
# {} is replaced to the single-quoted string of the focused line
fzf --preview 'cat {}'
```
Since preview window is updated only after the process is complete, it's
important that the command finishes quickly.
```bash
# Use head instead of cat so that the command doesn't take too long to finish
fzf --preview 'head -100 {}'
```
Preview window supports ANSI colors, so you can use programs that
syntax-highlights the content of a file.
- Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php
- CodeRay: http://coderay.rubychan.de/
- Rouge: https://github.com/jneen/rouge
```bash
# Try highlight, coderay, rougify in turn, then fall back to cat
fzf --preview '[[ $(file --mime {}) =~ binary ]] &&
echo {} is a binary file ||
(highlight -O ansi -l {} ||
coderay {} ||
rougify {} ||
cat {}) 2> /dev/null | head -500'
```
You can customize the size and position of the preview window using
`--preview-window` option. For example,
```bash
fzf --height 40% --reverse --preview 'file {}' --preview-window down:1
```
For more advanced examples, see [Key bindings for git with fzf][fzf-git].
[fzf-git]: https://junegunn.kr/2016/07/fzf-git/
Tips
----
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
[rg](https://github.com/BurntSushi/ripgrep) will do the
filtering:
```sh
@@ -426,10 +515,10 @@ export FZF_DEFAULT_COMMAND='
#### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
(will be fixed in 2.6.0) that it doesn't allow reading from STDIN in command
substitution, which means simple `vim (fzf)` won't work as expected. The
workaround is to use the `read` fish command:
Fish shell before version 2.6.0 [doesn't allow](https://github.com/fish-shell/fish-shell/issues/1362)
reading from STDIN in command substitution, which means simple `vim (fzf)`
doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use
the `read` fish command:
```sh
fzf | read -l result; and vim $result
@@ -457,7 +546,7 @@ make use of this feature. `$dir` defaults to `.` when the last token is not a
valid directory. Example:
```sh
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
```
[License](LICENSE)

View File

@@ -2,7 +2,7 @@
set -u
version=0.16.8
version=0.16.11
auto_completion=
key_bindings=
update_config=2
@@ -171,6 +171,7 @@ case "$archi" in
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
*) binary_available=0 binary_error=1 ;;
esac

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Jun 2017" "fzf 0.16.8" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Aug 2017" "fzf 0.16.11" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -111,6 +111,9 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.B "-m, --multi"
Enable multi-select with tab/shift-tab
.TP
.B "+m, --no-multi"
Disable multi-select
.TP
.B "--no-mouse"
Disable mouse
.TP
@@ -236,6 +239,7 @@ e.g. \fBfzf --color=bg+:24\fR
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
@@ -356,6 +360,9 @@ e.g. \fBfzf --multi | fzf --sync\fR
.B "--version"
Display version information and exit
.TP
Note that most options have the opposite versions with \fB--no-\fR prefix.
.SH ENVIRONMENT VARIABLES
.TP
.B FZF_DEFAULT_COMMAND

View File

@@ -149,13 +149,8 @@ function! s:tmux_enabled()
endfunction
function! s:escape(path)
let escaped_chars = '$%#''"'
if has('unix')
let escaped_chars .= ' \'
endif
return escape(a:path, escaped_chars)
let path = fnameescape(a:path)
return s:is_win ? escape(path, '$') : path
endfunction
" Upgrade legacy options
@@ -361,11 +356,14 @@ try
if has('nvim') && !has_key(dict, 'dir')
let dict.dir = s:fzf_getcwd()
endif
if has('win32unix') && has_key(dict, 'dir')
let dict.dir = fnamemodify(dict.dir, ':p')
endif
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '')
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
let temps.source = s:fzf_tempname()
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source)
endif
if has_key(dict, 'source')
@@ -526,12 +524,15 @@ function! s:execute(dict, command, use_height, temps) abort
let command = batchfile
let a:temps.batchfile = batchfile
if has('nvim')
let s:dict = a:dict
let s:temps = a:temps
let fzf = {}
let fzf.dict = a:dict
let fzf.temps = a:temps
function! fzf.on_exit(job_id, exit_status, event) dict
let lines = s:collect(s:temps)
call s:callback(s:dict, lines)
if s:present(self.dict, 'dir')
execute 'lcd' s:escape(self.dict.dir)
endif
let lines = s:collect(self.temps)
call s:callback(self.dict, lines)
endfunction
let cmd = 'start /wait cmd /c '.command
call jobstart(cmd, fzf)
@@ -687,7 +688,7 @@ function! s:execute_term(dict, command, temps) abort
lcd -
endif
endtry
setlocal nospell bufhidden=wipe nobuflisted
setlocal nospell bufhidden=wipe nobuflisted nonumber
setf fzf
startinsert
return []
@@ -755,7 +756,7 @@ let s:default_action = {
function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash)
return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
endfunction
function! s:cmd(bang, ...) abort
@@ -765,8 +766,6 @@ function! s:cmd(bang, ...) abort
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
if s:is_win && !&shellslash
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
elseif has('win32unix')
let opts.dir = fnamemodify(opts.dir, ':p')
endif
let prompt = opts.dir
else

View File

@@ -4,14 +4,9 @@ function fzf_key_bindings
# Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders"
set -l dir (commandline -t)
# The commandline token might be escaped, we need to unescape it.
set dir (eval "printf '%s' $dir")
if [ ! -d "$dir" ]
set dir .
end
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
set dir (string replace --regex '(.)/+$' '$1' "$dir")
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -l fzf_query $commandline[2]
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
@@ -19,19 +14,17 @@ function fzf_key_bindings
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
return
end
if [ "$dir" != . ]
else
# Remove last token from commandline.
commandline -t ""
end
@@ -46,22 +39,45 @@ function fzf_key_bindings
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
set -l FISH_MAJOR (echo $FISH_VERSION | cut -f1 -d.)
set -l FISH_MINOR (echo $FISH_VERSION | cut -f2 -d.)
# history's -z flag is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result
and commandline -- $result
else
history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
end
end
commandline -f repaint
end
function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -l fzf_query $commandline[2]
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
[ "$result" ]; and cd $result
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ]
cd $result
# Remove last token from commandline.
commandline -t ""
end
end
commandline -f repaint
end
@@ -84,4 +100,47 @@ function fzf_key_bindings
bind -M insert \cr fzf-history-widget
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'
# eval is used to do shell expansion on paths
set -l commandline (eval "printf '%s' "(commandline -t))
if [ -z $commandline ]
# Default to current directory with no --query
set dir '.'
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ]
# if $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline
else
# Also remove trailing slash after dir, to "split" input properly
set fzf_query (string replace -r "^$dir/?" '' "$commandline")
end
end
echo $dir
echo $fzf_query
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
set dir $argv
# Strip all trailing slashes. Ignore if $dir is root dir (/)
if [ (string length $dir) -gt 1 ]
set dir (string replace -r '/*$' '' $dir)
end
# Iteratively check if dir exists and strip tail end of path
while [ ! -d "$dir" ]
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
set dir (dirname "$dir")
end
echo $dir
end
end

View File

@@ -78,9 +78,11 @@ Scoring criteria
*/
import (
"bytes"
"fmt"
"strings"
"unicode"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
)
@@ -251,19 +253,72 @@ func normalizeRune(r rune) rune {
// 2. "pattern" is already normalized if "normalize" is true
type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
byteArray := input.Bytes()[from:]
idx := bytes.IndexByte(byteArray, b)
if idx == 0 {
// Can't skip any further
return from
}
// We may need to search for the uppercase letter again. We don't have to
// consider normalization as we can be sure that this is an ASCII string.
if !caseSensitive && b >= 'a' && b <= 'z' {
uidx := bytes.IndexByte(byteArray, b-32)
if idx < 0 || uidx >= 0 && uidx < idx {
idx = uidx
}
}
if idx < 0 {
return -1
}
return from + idx
}
func isAscii(runes []rune) bool {
for _, r := range runes {
if r >= utf8.RuneSelf {
return false
}
}
return true
}
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
// Can't determine
if !input.IsBytes() {
return 0
}
// Not possible
if !isAscii(pattern) {
return -1
}
firstIdx, idx := 0, 0
for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
if idx < 0 {
return -1
}
if pidx == 0 && idx > 0 {
// Step back to find the right bonus point
firstIdx = idx - 1
}
idx++
}
return firstIdx
}
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position.
// If the input string is too long, consider finding the matching chars in
// this phase as well (non-optimal alignment).
N := input.Length()
M := len(pattern)
switch M {
case 0:
if M == 0 {
return Result{0, 0, 0}, posArray(withPos, M)
case 1:
return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab)
}
N := input.Length()
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
@@ -281,10 +336,17 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
// Rune array
offset32, T := alloc32(offset32, slab, N, false)
// Phase 1. Check if there's a match and calculate bonus for each point
// Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(&input, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil
}
// Phase 2. Calculate bonus for each point
pidx, lastIdx, prevClass := 0, 0, charNonWord
for idx := 0; idx < N; idx++ {
char := input.Get(idx)
input.CopyRunes(T)
for ; idx < N; idx++ {
char := T[idx]
var class charClass
if char <= unicode.MaxASCII {
class = charClassOfAscii(char)
@@ -323,8 +385,17 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
if pidx != M {
return Result{-1, -1, 0}, nil
}
if M == 1 && B[F[0]] == bonusBoundary {
p := int(F[0])
result := Result{p, p + 1, scoreMatch + bonusBoundary*bonusFirstCharMultiplier}
if !withPos {
return result, nil
}
pos := []int{p}
return result, &pos
}
// Phase 2. Fill in score matrix (H)
// Phase 3. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission.
width := lastIdx - int(F[0]) + 1
offset16, H := alloc16(offset16, slab, width*M, false)
@@ -389,7 +460,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
if i == 0 {
fmt.Print(" ")
for j := int(F[i]); j <= lastIdx; j++ {
fmt.Printf(" " + string(input.Get(j)) + " ")
fmt.Printf(" " + string(T[j]) + " ")
}
fmt.Println()
}
@@ -413,7 +484,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
}
}
// Phase 3. (Optional) Backtrace to find character positions
// Phase 4. (Optional) Backtrace to find character positions
pos := posArray(withPos, M)
j := int(F[0])
if withPos {
@@ -606,6 +677,10 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
return Result{-1, -1, 0}, nil
}
if asciiFuzzyIndex(&text, pattern, caseSensitive) < 0 {
return Result{-1, -1, 0}, nil
}
// For simplicity, only look at the bonus at the first character position
pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)

View File

@@ -17,7 +17,7 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
if !caseSensitive {
pattern = strings.ToLower(pattern)
}
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
res, pos := fun(caseSensitive, normalize, forward, util.ToChars([]byte(input)), []rune(pattern), true, nil)
var start, end int
if pos == nil || len(*pos) == 0 {
start = res.Start

View File

@@ -44,7 +44,21 @@ func init() {
*/
// 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.|[\x0e\x0f]|.\x08")
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
}
func findAnsiStart(str string) int {
idx := 0
for ; idx < len(str); idx++ {
b := str[idx]
if b == 0x1b || b == 0x0e || b == 0x0f {
return idx
}
if b == 0x08 && idx > 0 {
return idx - 1
}
}
return idx
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -55,41 +69,61 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
}
idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
prev := str[idx:offset[0]]
output.WriteString(prev)
prevIdx := 0
runeCount := 0
for idx := 0; idx < len(str); {
idx += findAnsiStart(str[idx:])
// No sign of ANSI code
if idx == len(str) {
break
}
// Make sure that we found an ANSI code
offset := ansiRegex.FindStringIndex(str[idx:])
if offset == nil {
idx++
continue
}
offset[0] += idx
offset[1] += idx
idx = offset[1]
// Check if we should continue
prev := str[prevIdx:offset[0]]
if proc != nil && !proc(prev, state) {
return "", nil, nil
}
newState := interpretCode(str[offset[0]:offset[1]], state)
prevIdx = offset[1]
runeCount += utf8.RuneCountInString(prev)
output.WriteString(prev)
newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) {
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if newState.colored() {
// Append new offset
state = newState
newLen := int32(utf8.RuneCount(output.Bytes()))
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
} else {
// Discard state
state = nil
}
}
idx = offset[1]
}
rest := str[idx:]
rest := str[prevIdx:]
if len(rest) > 0 {
output.WriteString(rest)
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
}
if proc != nil {

View File

@@ -3,7 +3,7 @@ package fzf
import "sync"
// queryCache associates strings to lists of items
type queryCache map[string][]*Result
type queryCache map[string][]Result
// ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct {
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
}
// Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return
}
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
(*qc)[key] = list
}
// Find is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
// Lookup is called to lookup ChunkCache
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
if len(key) == 0 || !chunk.IsFull() {
return nil, false
return nil
}
cc.mutex.Lock()
@@ -46,8 +46,36 @@ func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
if ok {
list, ok := (*qc)[key]
if ok {
return list, true
return list
}
}
return nil, false
return nil
}
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
if len(key) == 0 || !chunk.IsFull() {
return nil
}
cc.mutex.Lock()
defer cc.mutex.Unlock()
qc, ok := cc.cache[chunk]
if !ok {
return nil
}
for idx := 1; idx < len(key); idx++ {
// [---------| ] | [ |---------]
// [--------| ] | [ |--------]
// [-------| ] | [ |-------]
prefix := key[:len(key)-idx]
suffix := key[idx:]
for _, substr := range [2]string{prefix, suffix} {
if cached, found := (*qc)[substr]; found {
return cached
}
}
}
return nil
}

View File

@@ -7,34 +7,34 @@ func TestChunkCache(t *testing.T) {
chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{}
chunk2p := &chunk2
items1 := []*Result{&Result{}}
items2 := []*Result{&Result{}, &Result{}}
items1 := []Result{Result{}}
items2 := []Result{Result{}, Result{}}
cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2)
{ // chunk1 is not full
cached, found := cache.Find(chunk1p, "foo")
if found {
t.Error("Cached disabled for non-empty chunks", found, cached)
cached := cache.Lookup(chunk1p, "foo")
if cached != nil {
t.Error("Cached disabled for non-empty chunks", cached)
}
}
{
cached, found := cache.Find(chunk2p, "foo")
if !found || len(cached) != 1 {
t.Error("Expected 1 item cached", found, cached)
cached := cache.Lookup(chunk2p, "foo")
if cached == nil || len(cached) != 1 {
t.Error("Expected 1 item cached", cached)
}
}
{
cached, found := cache.Find(chunk2p, "bar")
if !found || len(cached) != 2 {
t.Error("Expected 2 items cached", found, cached)
cached := cache.Lookup(chunk2p, "bar")
if cached == nil || len(cached) != 2 {
t.Error("Expected 2 items cached", cached)
}
}
{
cached, found := cache.Find(chunk1p, "foobar")
if found {
t.Error("Expected 0 item cached", found, cached)
cached := cache.Lookup(chunk1p, "foobar")
if cached != nil {
t.Error("Expected 0 item cached", cached)
}
}
}

View File

@@ -2,12 +2,12 @@ package fzf
import "sync"
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
type Chunk []*Item // >>> []Item
// Chunk is a list of Items whose size has the upper limit of chunkSize
type Chunk []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a
// string and an integer
type ItemBuilder func([]byte, int) *Item
type ItemBuilder func([]byte, int) Item
// ChunkList is a list of Chunks
type ChunkList struct {
@@ -28,11 +28,11 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
item := trans(data, index)
if item != nil {
*c = append(*c, item)
return true
if item.Nil() {
return false
}
return false
*c = append(*c, item)
return true
}
// IsFull returns true if the Chunk is full
@@ -55,39 +55,35 @@ func CountItems(cs []*Chunk) int {
// Push adds the item to the list
func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock()
defer cl.mutex.Unlock()
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
newChunk := Chunk(make([]*Item, 0, chunkSize))
newChunk := Chunk(make([]Item, 0, chunkSize))
cl.chunks = append(cl.chunks, &newChunk)
}
if cl.lastChunk().push(cl.trans, data, cl.count) {
cl.count++
cl.mutex.Unlock()
return true
}
cl.mutex.Unlock()
return false
}
// Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock()
defer cl.mutex.Unlock()
ret := make([]*Chunk, len(cl.chunks))
count := cl.count
copy(ret, cl.chunks)
// Duplicate the last chunk
if cnt := len(ret); cnt > 0 {
ret[cnt-1] = ret[cnt-1].dupe()
newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk
}
return ret, cl.count
}
func (c *Chunk) dupe() *Chunk {
newChunk := make(Chunk, len(*c))
for idx, ptr := range *c {
newChunk[idx] = ptr
}
return &newChunk
cl.mutex.Unlock()
return ret, count
}

View File

@@ -11,8 +11,10 @@ func TestChunkList(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byLength}
cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: util.ToChars(s), index: int32(i * 2)}
cl := NewChunkList(func(s []byte, i int) Item {
chars := util.ToChars(s)
chars.Index = int32(i * 2)
return Item{text: chars}
})
// Snapshot
@@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items")
}
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 ||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 {
t.Error("Invalid data")
}
if chunk1.IsFull() {

View File

@@ -9,7 +9,7 @@ import (
const (
// Current version
version = "0.16.8"
version = "0.16.11"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -63,67 +63,51 @@ func Run(opts *Options, revision string) {
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil
}
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
return util.RunesToChars(data), nil
}
if opts.Ansi {
if opts.Theme != nil {
var state *ansiState
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState
return util.RunesToChars([]rune(trimmed)), offsets
return util.ToChars([]byte(trimmed)), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil)
return util.RunesToChars([]rune(trimmed)), nil
return util.ToChars([]byte(trimmed)), nil
}
}
ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
}
// Chunk list
var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data []byte, index int) *Item {
chunkList = NewChunkList(func(data []byte, index int) Item {
if len(header) < opts.HeaderLines {
header = append(header, string(data))
eventBox.Set(EvtHeader, header)
return nil
return nilItem
}
chars, colors := ansiProcessor(data)
return &Item{
index: int32(index),
trimLength: -1,
text: chars,
colors: colors}
chars.Index = int32(index)
return Item{text: chars, colors: colors}
})
} else {
chunkList = NewChunkList(func(data []byte, index int) *Item {
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
chunkList = NewChunkList(func(data []byte, index int) Item {
tokens := Tokenize(string(data), opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans)))
header = append(header, transformed)
eventBox.Set(EvtHeader, header)
return nil
return nilItem
}
textRunes := joinTokens(trans)
item := Item{
index: int32(index),
trimLength: -1,
origText: &data,
colors: nil}
trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed
item.colors = colors
return &item
trimmed, colors := ansiProcessor([]byte(transformed))
trimmed.Index = int32(index)
return Item{text: trimmed, colors: colors, origText: &data}
})
}
@@ -168,8 +152,8 @@ func Run(opts *Options, revision string) {
reader := Reader{
func(runes []byte) bool {
item := chunkList.trans(runes, 0)
if item != nil {
if result, _, _ := pattern.MatchItem(item, false, slab); result != nil {
if !item.Nil() {
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString())
found = true
}
@@ -221,14 +205,13 @@ func Run(opts *Options, revision string) {
delay := true
ticks++
eventBox.Wait(func(events *util.Events) {
defer events.Clear()
for evt, value := range *events {
switch evt {
case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading)
terminal.UpdateCount(count, !reading, value.(bool))
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew:
@@ -281,6 +264,7 @@ func Run(opts *Options, revision string) {
}
}
}
events.Clear()
})
if delay && reading {
dur := util.DurWithin(

View File

@@ -4,27 +4,27 @@ import (
"github.com/junegunn/fzf/src/util"
)
// Item represents each input line
// Item represents each input line. 56 bytes.
type Item struct {
index int32
trimLength int32
text util.Chars
origText *[]byte
colors *[]ansiOffset
transformed []Token
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
transformed *[]Token // 8
origText *[]byte // 8
colors *[]ansiOffset // 8
}
// Index returns ordinal index of the Item
func (item *Item) Index() int32 {
return item.index
return item.text.Index
}
func (item *Item) TrimLength() int32 {
if item.trimLength >= 0 {
return item.trimLength
}
item.trimLength = int32(item.text.TrimLength())
return item.trimLength
var nilItem = Item{text: util.Chars{Index: -1}}
func (item *Item) Nil() bool {
return item.Index() < 0
}
func (item *Item) TrimLength() uint16 {
return item.text.TrimLength()
}
// Colors returns ansiOffsets of the Item

View File

@@ -131,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct {
index int
matches []*Result
matches []Result
}
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@@ -162,7 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
go func(idx int, slab *util.Slab, chunks []*Chunk) {
defer func() { waitGroup.Done() }()
count := 0
allMatches := make([][]*Result, len(chunks))
allMatches := make([][]Result, len(chunks))
for idx, chunk := range chunks {
matches := request.pattern.Match(chunk, slab)
allMatches[idx] = matches
@@ -172,7 +172,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
countChan <- len(matches)
}
sliceMatches := make([]*Result, 0, count)
sliceMatches := make([]Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
@@ -212,7 +212,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
}
partialResults := make([][]*Result, numSlices)
partialResults := make([][]Result, numSlices)
for _ = range slices {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches

View File

@@ -3,14 +3,14 @@ package fzf
import "fmt"
// EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list
type Merger struct {
pattern *Pattern
lists [][]*Result
merged []*Result
lists [][]Result
merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
@@ -35,11 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
}
// NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
mg := Merger{
pattern: pattern,
lists: lists,
merged: []*Result{},
merged: []Result{},
chunks: nil,
cursors: make([]int, len(lists)),
sorted: sorted,
@@ -59,13 +59,13 @@ func (mg *Merger) Length() int {
}
// Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) *Result {
func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil {
if mg.tac {
idx = mg.count - idx - 1
}
chunk := (*mg.chunks)[idx/chunkSize]
return &Result{item: (*chunk)[idx%chunkSize]}
return Result{item: &(*chunk)[idx%chunkSize]}
}
if mg.sorted {
@@ -89,7 +89,7 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax
}
func (mg *Merger) mergedGet(idx int) *Result {
func (mg *Merger) mergedGet(idx int) Result {
for i := len(mg.merged); i <= idx; i++ {
minRank := minRank()
minIdx := -1
@@ -100,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Result {
continue
}
if cursor >= 0 {
rank := list[cursor].rank
rank := list[cursor]
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank
minIdx = listIdx

View File

@@ -15,11 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
}
}
func randResult() *Result {
func randResult() Result {
str := fmt.Sprintf("%d", rand.Uint32())
return &Result{
item: &Item{text: util.RunesToChars([]rune(str))},
rank: rank{index: rand.Int31()}}
chars := util.ToChars([]byte(str))
chars.Index = rand.Int31()
return Result{item: &Item{text: chars}}
}
func TestEmptyMerger(t *testing.T) {
@@ -29,14 +29,14 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
}
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4
lists := make([][]*Result, numLists)
lists := make([][]Result, numLists)
cnt := 0
for i := 0; i < numLists; i++ {
numResults := rand.Int() % 20
cnt += numResults
lists[i] = make([]*Result, numResults)
lists[i] = make([]Result, numResults)
for j := 0; j < numResults; j++ {
item := randResult()
lists[i][j] = item
@@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
sort.Sort(ByRelevance(lists[i]))
}
}
items := []*Result{}
items := []Result{}
for _, list := range lists {
items = append(items, list...)
}

View File

@@ -963,6 +963,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--no-expect":
opts.Expect = make(map[int]string)
case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":

View File

@@ -6,7 +6,6 @@ import (
"testing"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
func TestDelimiterRegex(t *testing.T) {
@@ -44,7 +43,7 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
tokens := Tokenize("-*--*---**---", delim)
if delim.regex != nil ||
tokens[0].text.ToString() != "-*" ||
tokens[1].text.ToString() != "--*" ||
@@ -57,7 +56,7 @@ func TestDelimiterRegexString(t *testing.T) {
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
tokens := Tokenize("-*--*---**---", delim)
if delim.str != nil ||
tokens[0].text.ToString() != "-*--*" ||
tokens[1].text.ToString() != "---*" ||

View File

@@ -243,31 +243,17 @@ func (p *Pattern) CacheKey() string {
}
// Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result {
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match
cacheKey := p.CacheKey()
if p.cacheable {
if cached, found := _cache.Find(chunk, cacheKey); found {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
return cached
}
}
// Prefix/suffix cache
var space []*Result
Loop:
for idx := 1; idx < len(cacheKey); idx++ {
// [---------| ] | [ |---------]
// [--------| ] | [ |--------]
// [-------| ] | [ |-------]
prefix := cacheKey[:len(cacheKey)-idx]
suffix := cacheKey[idx:]
for _, substr := range [2]*string{&prefix, &suffix} {
if cached, found := _cache.Find(chunk, *substr); found {
space = cached
break Loop
}
}
}
space := _cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab)
@@ -277,19 +263,19 @@ Loop:
return matches
}
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result {
matches := []*Result{}
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []Result{}
if space == nil {
for _, item := range *chunk {
if match, _, _ := p.MatchItem(item, false, slab); match != nil {
matches = append(matches, match)
for idx := range *chunk {
if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
matches = append(matches, match)
matches = append(matches, *match)
}
}
}
@@ -300,20 +286,27 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if p.extended {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
return buildResult(item, offsets, bonus), offsets, pos
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
return nil, nil, nil
}
offset, bonus, pos := p.basicMatch(item, withPos, slab)
if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
return buildResult(item, offsets, bonus), offsets, pos
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
return nil, nil, nil
}
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
input := p.prepareInput(item)
var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
if p.fuzzy {
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
}
@@ -321,7 +314,12 @@ 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) {
input := p.prepareInput(item)
var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
offsets := []Offset{}
var totalScore int
var allPos *[]int
@@ -365,19 +363,14 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
return offsets, totalScore, allPos
}
func (p *Pattern) prepareInput(item *Item) []Token {
func (p *Pattern) transformInput(item *Item) []Token {
if item.transformed != nil {
return item.transformed
return *item.transformed
}
var ret []Token
if len(p.nth) == 0 {
ret = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
}
item.transformed = ret
tokens := Tokenize(item.text.ToString(), p.delimiter)
ret := Transform(tokens, p.nth)
item.transformed = &ret
return ret
}

View File

@@ -78,7 +78,7 @@ func TestExact(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc"))
res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
}
@@ -94,7 +94,7 @@ func TestEqual(t *testing.T) {
match := func(str string, sidxExpected int, eidxExpected int) {
res, pos := algo.EqualMatch(
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte(str)), pattern.termSets[0][0].text, true, nil)
if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
}
@@ -133,30 +133,30 @@ 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(util.RunesToChars([]rune("junegunn")), Delimiter{})
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}})
origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} {
chunk := Chunk{
&Item{
text: util.RunesToChars([]rune("junegunn")),
Item{
text: util.ToChars([]byte("junegunn")),
origText: &origBytes,
transformed: trans},
transformed: &trans},
}
pattern.extended = extended
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
if !(matches[0].item.text.ToString() == "junegunn" &&
string(*matches[0].item.origText) == "junegunn.choi" &&
reflect.DeepEqual(matches[0].item.transformed, trans)) {
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
t.Error("Invalid match result", matches)
}
match, offsets, pos := pattern.MatchItem(chunk[0], true, slab)
match, offsets, pos := pattern.MatchItem(&chunk[0], true, slab)
if !(match.item.text.ToString() == "junegunn" &&
string(*match.item.origText) == "junegunn.choi" &&
offsets[0][0] == 0 && offsets[0][1] == 5 &&
reflect.DeepEqual(match.item.transformed, trans)) {
reflect.DeepEqual(*match.item.transformed, trans)) {
t.Error("Invalid match result", match, offsets, extended)
}
if !((*pos)[0] == 4 && (*pos)[1] == 0) {

View File

@@ -17,16 +17,17 @@ type Reader struct {
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource() {
var success bool
if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
cmd = defaultCommand
}
r.readFromCommand(cmd)
success = r.readFromCommand(cmd)
} else {
r.readFromStdin()
success = r.readFromStdin()
}
r.eventBox.Set(EvtReadFin, nil)
r.eventBox.Set(EvtReadFin, success)
}
func (r *Reader) feed(src io.Reader) {
@@ -50,7 +51,7 @@ func (r *Reader) feed(src io.Reader) {
}
}
if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil)
r.eventBox.Set(EvtReadNew, true)
}
}
if err != nil {
@@ -59,20 +60,21 @@ func (r *Reader) feed(src io.Reader) {
}
}
func (r *Reader) readFromStdin() {
func (r *Reader) readFromStdin() bool {
r.feed(os.Stdin)
return true
}
func (r *Reader) readFromCommand(cmd string) {
func (r *Reader) readFromCommand(cmd string) bool {
listCommand := util.ExecCommand(cmd)
out, err := listCommand.StdoutPipe()
if err != nil {
return
return false
}
err = listCommand.Start()
if err != nil {
return
return false
}
defer listCommand.Wait()
r.feed(out)
return listCommand.Wait() == nil
}

View File

@@ -19,22 +19,17 @@ type colorOffset struct {
index int32
}
type rank struct {
points [4]uint16
index int32
}
type Result struct {
item *Item
rank rank
item *Item
points [4]uint16
}
func buildResult(item *Item, offsets []Offset, score int) *Result {
func buildResult(item *Item, offsets []Offset, score int) Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item, rank: rank{index: item.index}}
result := Result{item: item}
numChars := item.text.Length()
minBegin := math.MaxUint16
minEnd := math.MaxUint16
@@ -57,7 +52,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
// Higher is better
val = math.MaxUint16 - util.AsUint16(score)
case byLength:
val = util.AsUint16(int(item.TrimLength()))
val = item.TrimLength()
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
@@ -75,10 +70,10 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
}
}
}
result.rank.points[idx] = val
result.points[idx] = val
}
return &result
return result
}
// Sort criteria to use. Never changes once fzf is started.
@@ -86,11 +81,11 @@ var sortCriteria []criterion
// Index returns ordinal index of the Item
func (result *Result) Index() int32 {
return result.item.index
return result.item.Index()
}
func minRank() rank {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
func minRank() Result {
return Result{item: &nilItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
@@ -201,7 +196,7 @@ func (a ByOrder) Less(i, j int) bool {
}
// ByRelevance is for sorting Items
type ByRelevance []*Result
type ByRelevance []Result
func (a ByRelevance) Len() int {
return len(a)
@@ -212,11 +207,11 @@ func (a ByRelevance) Swap(i, j int) {
}
func (a ByRelevance) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
return compareRanks(a[i], a[j], false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Result
type ByRelevanceTac []Result
func (a ByRelevanceTac) Len() int {
return len(a)
@@ -227,10 +222,10 @@ func (a ByRelevanceTac) Swap(i, j int) {
}
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
return compareRanks(a[i], a[j], true)
}
func compareRanks(irank rank, jrank rank, tac bool) bool {
func compareRanks(irank Result, jrank Result, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank.points[idx]
right := jrank.points[idx]
@@ -240,5 +235,5 @@ func compareRanks(irank rank, jrank rank, tac bool) bool {
return false
}
}
return (irank.index <= jrank.index) != tac
return (irank.item.Index() <= jrank.item.Index()) != tac
}

View File

@@ -11,6 +11,11 @@ import (
"github.com/junegunn/fzf/src/util"
)
func withIndex(i *Item, index int) *Item {
(*i).text.Index = int32(index)
return i
}
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
@@ -26,10 +31,10 @@ func TestOffsetSort(t *testing.T) {
}
func TestRankComparison(t *testing.T) {
rank := func(vals ...uint16) rank {
return rank{
rank := func(vals ...uint16) Result {
return Result{
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
index: int32(vals[4])}
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
@@ -52,36 +57,41 @@ func TestResultRank(t *testing.T) {
sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2)
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus
item1.rank.points[1] != 3 || // Length
item1.rank.points[2] != 0 || // Unused
item1.rank.points[3] != 0 || // Unused
item1.item.index != 1 {
t.Error(item1.rank)
item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
if item1.points[0] != math.MaxUint16-2 || // Bonus
item1.points[1] != 3 || // Length
item1.points[2] != 0 || // Unused
item1.points[3] != 0 || // Unused
item1.item.Index() != 1 {
t.Error(item1)
}
// Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
items := []*Result{item1, item2}
items := []Result{item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item1 {
t.Error(items)
}
items = []*Result{item2, item1, item1, item2}
items = []Result{item2, item1, item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item2 ||
items[2] != item1 || items[3] != item1 {
t.Error(items, item1, item1.item.index, item2, item2.item.index)
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
}
// Sort by relevance
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
items = []*Result{item1, item2, item3, item4, item5, item6}
item3 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
item4 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
items = []Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 &&
items[2] == item4 && items[3] == item3 &&

View File

@@ -87,6 +87,7 @@ type Terminal struct {
margin [4]sizeSpec
strong tui.Attr
bordered bool
cleanExit bool
border tui.Window
window tui.Window
pborder tui.Window
@@ -94,11 +95,13 @@ type Terminal struct {
count int
progress int
reading bool
success bool
jumping jumpMode
jumpLabels string
printer func(string)
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
preview previewOpts
previewer previewer
@@ -365,6 +368,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
history: opts.History,
margin: opts.Margin,
bordered: opts.Bordered,
cleanExit: opts.ClearOnExit,
strong: strongAttr,
cycle: opts.Cycle,
header: header,
@@ -372,6 +376,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
success: true,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
@@ -401,10 +406,11 @@ func (t *Terminal) Input() []rune {
}
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool) {
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.success = success
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
if final {
@@ -682,6 +688,9 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
if !t.success && t.count == 0 {
output += " [ERROR]"
}
if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output)
}
@@ -704,11 +713,11 @@ func (t *Terminal) printHeader() {
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState
item := &Item{
text: util.RunesToChars([]rune(trimmed)),
text: util.ToChars([]byte(trimmed)),
colors: colors}
t.move(line, 2, true)
t.printHighlighted(&Result{item: item},
t.printHighlighted(Result{item: item},
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false)
}
}
@@ -736,7 +745,7 @@ func (t *Terminal) printList() {
}
}
func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
func (t *Terminal) printItem(result Result, line int, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := " "
@@ -752,7 +761,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
// Avoid unnecessary redraw
newLine := itemLine{current: current, selected: selected, label: label,
result: *result, queryLen: len(t.input), width: 0}
result: result, queryLen: len(t.input), width: 0}
prevLine := t.prevLines[i]
if prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
@@ -834,7 +843,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
}
func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
item := result.item
// Overflow
@@ -954,6 +963,7 @@ func (t *Terminal) printPreview() {
}
reader := bufio.NewReader(strings.NewReader(t.previewer.text))
lineNo := -t.previewer.offset
height := t.pwindow.Height()
var ansi *ansiState
for {
line, err := reader.ReadString('\n')
@@ -962,7 +972,8 @@ func (t *Terminal) printPreview() {
line = line[:len(line)-1]
}
lineNo++
if lineNo > t.pwindow.Height() {
if lineNo > height ||
t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
break
} else if lineNo > 0 {
var fillRet tui.FillReturn
@@ -992,7 +1003,7 @@ func (t *Terminal) printPreview() {
}
}
t.pwindow.FinishFill()
if t.previewer.lines > t.pwindow.Height() {
if t.previewer.lines > height {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
pos := t.pwindow.Width() - len(offset)
if t.tui.DoesAutoWrap() {
@@ -1163,8 +1174,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
}
for idx, item := range items {
chars := util.RunesToChars([]rune(item.AsString(stripAnsi)))
tokens := Tokenize(chars, delimiter)
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := string(joinTokens(trans))
if delimiter.str != nil {
@@ -1248,6 +1258,24 @@ func (t *Terminal) truncateQuery() {
t.cx = util.Constrain(t.cx, 0, len(t.input))
}
func (t *Terminal) selectItem(item *Item) {
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) toggleItem(item *Item) {
if _, found := t.selected[item.Index()]; !found {
t.selectItem(item)
} else {
t.deselectItem(item)
}
}
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
// prof := profile.Start(profile.ProfilePath("/tmp/"))
@@ -1335,7 +1363,12 @@ func (t *Terminal) Loop() {
}()
}
exit := func(code int) {
exit := func(getCode func() int) {
if !t.cleanExit && t.fullscreen && t.inlineInfo {
t.placeCursor()
}
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
@@ -1345,6 +1378,7 @@ func (t *Terminal) Loop() {
go func() {
var focused *Item
var version int64
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
@@ -1361,7 +1395,8 @@ func (t *Terminal) Loop() {
case reqList:
t.printList()
currentFocus := t.currentItem()
if currentFocus != focused {
if currentFocus != focused || version != t.version {
version = t.version
focused = currentFocus
if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false)
@@ -1383,11 +1418,12 @@ func (t *Terminal) Loop() {
case reqRedraw:
t.redraw()
case reqClose:
t.tui.Close()
if t.output() {
exit(exitOk)
}
exit(exitNoMatch)
exit(func() int {
if t.output() {
return exitOk
}
return exitNoMatch
})
case reqPreviewDisplay:
t.previewer.text = value.(string)
t.previewer.lines = strings.Count(t.previewer.text, "\n")
@@ -1396,12 +1432,12 @@ func (t *Terminal) Loop() {
case reqPreviewRefresh:
t.printPreview()
case reqPrintQuery:
t.tui.Close()
t.printer(string(t.input))
exit(exitOk)
exit(func() int {
t.printer(string(t.input))
return exitOk
})
case reqQuit:
t.tui.Close()
exit(exitInterrupt)
exit(func() int { return exitInterrupt })
}
}
t.placeCursor()
@@ -1426,22 +1462,9 @@ func (t *Terminal) Loop() {
}
}
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y).item
if !selectItem(item) {
delete(t.selected, item.Index())
}
}
toggle := func() {
if t.cy < t.merger.Length() {
toggleY(t.cy)
t.toggleItem(t.merger.Get(t.cy).item)
req(reqInfo)
}
}
@@ -1555,17 +1578,14 @@ func (t *Terminal) Loop() {
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i).item
selectItem(item)
t.selectItem(t.merger.Get(i).item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.Index())
}
t.selected = make(map[int32]selectedItem)
t.version++
req(reqList, reqInfo)
}
case actToggle:
@@ -1576,7 +1596,7 @@ func (t *Terminal) Loop() {
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
t.toggleItem(t.merger.Get(i).item)
}
req(reqList, reqInfo)
}

View File

@@ -10,7 +10,7 @@ import (
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
func TestReplacePlaceholder(t *testing.T) {

View File

@@ -1,6 +1,7 @@
package fzf
import (
"bytes"
"regexp"
"strconv"
"strings"
@@ -74,14 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true
}
func withPrefixLengths(tokens []util.Chars, begin int) []Token {
func withPrefixLengths(tokens []string, begin int) []Token {
ret := make([]Token, len(tokens))
prefixLength := begin
for idx, token := range tokens {
// NOTE: &tokens[idx] instead of &tokens
ret[idx] = Token{&tokens[idx], int32(prefixLength)}
prefixLength += token.Length()
for idx := range tokens {
chars := util.ToChars([]byte(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length()
}
return ret
}
@@ -92,16 +93,15 @@ const (
awkWhite
)
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
func awkTokenizer(input string) ([]string, int) {
// 9, 32
ret := []util.Chars{}
ret := []string{}
prefixLength := 0
state := awkNil
numChars := input.Length()
begin := 0
end := 0
for idx := 0; idx < numChars; idx++ {
r := input.Get(idx)
for idx := 0; idx < len(input); idx++ {
r := input[idx]
white := r == 9 || r == 32
switch state {
case awkNil:
@@ -119,19 +119,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) {
if white {
end = idx + 1
} else {
ret = append(ret, input.Slice(begin, end))
ret = append(ret, input[begin:end])
state, begin, end = awkBlack, idx, idx+1
}
}
}
if begin < end {
ret = append(ret, input.Slice(begin, end))
ret = append(ret, input[begin:end])
}
return ret, prefixLength
}
// Tokenize tokenizes the given string with the delimiter
func Tokenize(text util.Chars, delimiter Delimiter) []Token {
func Tokenize(text string, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(text)
@@ -139,36 +139,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token {
}
if delimiter.str != nil {
return withPrefixLengths(text.Split(*delimiter.str), 0)
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
}
// FIXME performance
var tokens []string
if delimiter.regex != nil {
str := text.ToString()
for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str)
for len(text) > 0 {
loc := delimiter.regex.FindStringIndex(text)
if loc == nil {
loc = []int{0, len(str)}
loc = []int{0, len(text)}
}
last := util.Max(loc[1], 1)
tokens = append(tokens, str[:last])
str = str[last:]
tokens = append(tokens, text[:last])
text = text[last:]
}
}
asRunes := make([]util.Chars, len(tokens))
for i, token := range tokens {
asRunes[i] = util.RunesToChars([]rune(token))
}
return withPrefixLengths(asRunes, 0)
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens []Token) []rune {
ret := []rune{}
func joinTokens(tokens []Token) string {
var output bytes.Buffer
for _, token := range tokens {
ret = append(ret, token.text.ToRunes()...)
output.WriteString(token.text.ToString())
}
return ret
return output.String()
}
// Transform is used to transform the input when --with-nth option is given
@@ -181,7 +176,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
chars := util.RunesToChars(joinTokens(tokens))
chars := util.ToChars([]byte(joinTokens(tokens)))
parts = append(parts, &chars)
} else {
if idx < 0 {
@@ -224,15 +219,15 @@ func Transform(tokens []Token, withNth []Range) []Token {
var merged util.Chars
switch len(parts) {
case 0:
merged = util.RunesToChars([]rune{})
merged = util.ToChars([]byte{})
case 1:
merged = *parts[0]
default:
runes := []rune{}
var output bytes.Buffer
for _, part := range parts {
runes = append(runes, part.ToRunes()...)
output.WriteString(part.ToString())
}
merged = util.RunesToChars(runes)
merged = util.ToChars([]byte(output.String()))
}
var prefixLength int32

View File

@@ -2,8 +2,6 @@ package fzf
import (
"testing"
"github.com/junegunn/fzf/src/util"
)
func TestParseRange(t *testing.T) {
@@ -47,19 +45,19 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) {
// AWK-style
input := " abc: def: ghi "
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
tokens := Tokenize(input, Delimiter{})
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens)
}
// With delimiter
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
tokens = Tokenize(input, delimiterRegexp(":"))
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
t.Errorf("%s", tokens)
t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
}
// With delimiter regex
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
tokens = Tokenize(input, delimiterRegexp("\\s+"))
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
@@ -71,7 +69,7 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl"
{
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
tokens := Tokenize(input, Delimiter{})
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
@@ -93,7 +91,7 @@ func TestTransform(t *testing.T) {
}
}
{
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
tokens := Tokenize(input, delimiterRegexp(":"))
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)

View File

@@ -32,7 +32,8 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R"
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open " + consoleDevice)
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
}
return in
}
@@ -182,10 +183,18 @@ func (r *LightRenderer) Init() {
if r.fullscreen {
r.smcup()
} else {
r.csi("J")
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
y, x := r.findOffset()
r.mouse = r.mouse && y >= 0
if x > 0 {
// When --no-clear is used for repetitive relaunching, there is a small
// time frame between fzf processes where the user keystrokes are not
// captured by either of fzf process which can cause x offset to be
// increased and we're left with unwanted extra new line.
if x > 0 && r.clearOnExit {
r.upOneLine = true
r.makeSpace()
}
@@ -200,7 +209,7 @@ func (r *LightRenderer) Init() {
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
// r.csi("s")
r.csi("s")
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
@@ -411,10 +420,12 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{F12, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
// Bracketed paste mode: \e[200~ ... \e[201~
if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
// Immediately discard the sequence from the buffer and reread input
r.buffer = r.buffer[6:]
*sz = 0
return r.GetChar()
}
return Event{Invalid, 0, nil} // INS
case 51:
@@ -584,10 +595,8 @@ func (r *LightRenderer) Close() {
}
r.csi("J")
}
} else if r.fullscreen {
r.csi("G")
} else {
r.move(r.height, 0)
} else if !r.fullscreen {
r.csi("u")
}
if r.mouse {
r.csi("?1000l")
@@ -697,6 +706,10 @@ func (w *LightWindow) X() int {
return w.posx
}
func (w *LightWindow) Y() int {
return w.posy
}
func (w *LightWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
@@ -831,17 +844,20 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 {
w.MoveAndClear(w.posy+1, 0)
w.Move(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth
// Wrap line
if j < len(lines)-1 || i < len(allLines)-1 {
if w.posy+1 >= w.height {
return FillSuspend
}
w.MoveAndClear(w.posy+1, 0)
w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0)
onMove()
}
}
@@ -856,24 +872,25 @@ func (w *LightWindow) setBg() {
}
func (w *LightWindow) Fill(text string) FillReturn {
w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy, w.posx)
w.setBg()
return w.fill(text, w.setBg)
}
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy, w.posx)
if bg == colDefault {
bg = w.bg
}
if w.csiColor(fg, bg, attr) {
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
}
return w.fill(text, w.setBg)
}
func (w *LightWindow) FinishFill() {
w.MoveAndClear(w.posy, w.posx)
for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0)
}

View File

@@ -164,6 +164,10 @@ func (w *TcellWindow) X() int {
return w.lastX
}
func (w *TcellWindow) Y() int {
return w.lastY
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false
}

View File

@@ -236,6 +236,7 @@ type Window interface {
Close()
X() int
Y() int
Enclose(y int, x int) bool
Move(y int, x int)

View File

@@ -3,63 +3,103 @@ package util
import (
"unicode"
"unicode/utf8"
"unsafe"
)
const (
overflow64 uint64 = 0x8080808080808080
overflow32 uint32 = 0x80808080
)
type Chars struct {
runes []rune
bytes []byte
slice []byte // or []rune
inBytes bool
trimLengthKnown bool
trimLength uint16
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
// minimize the memory footprint by not wasting padded spaces.
Index int32
}
func checkAscii(bytes []byte) (bool, int) {
i := 0
for ; i <= len(bytes)-8; i += 8 {
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i <= len(bytes)-4; i += 4 {
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i < len(bytes); i++ {
if bytes[i] >= utf8.RuneSelf {
return false, i
}
}
return true, 0
}
// ToChars converts byte array into rune array
func ToChars(bytea []byte) Chars {
var runes []rune
ascii := true
numBytes := len(bytea)
for i := 0; i < numBytes; {
if bytea[i] < utf8.RuneSelf {
if !ascii {
runes = append(runes, rune(bytea[i]))
}
i++
} else {
if ascii {
ascii = false
runes = make([]rune, i, numBytes)
for j := 0; j < i; j++ {
runes[j] = rune(bytea[j])
}
}
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
func ToChars(bytes []byte) Chars {
inBytes, bytesUntil := checkAscii(bytes)
if inBytes {
return Chars{slice: bytes, inBytes: inBytes}
}
if ascii {
return Chars{bytes: bytea}
runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ {
runes[i] = rune(bytes[i])
}
return Chars{runes: runes}
for i := bytesUntil; i < len(bytes); {
r, sz := utf8.DecodeRune(bytes[i:])
i += sz
runes = append(runes, r)
}
return RunesToChars(runes)
}
func RunesToChars(runes []rune) Chars {
return Chars{runes: runes}
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
}
func (chars *Chars) IsBytes() bool {
return chars.inBytes
}
func (chars *Chars) Bytes() []byte {
return chars.slice
}
func (chars *Chars) optionalRunes() []rune {
if chars.inBytes {
return nil
}
return *(*[]rune)(unsafe.Pointer(&chars.slice))
}
func (chars *Chars) Get(i int) rune {
if chars.runes != nil {
return chars.runes[i]
if runes := chars.optionalRunes(); runes != nil {
return runes[i]
}
return rune(chars.bytes[i])
return rune(chars.slice[i])
}
func (chars *Chars) Length() int {
if chars.runes != nil {
return len(chars.runes)
if runes := chars.optionalRunes(); runes != nil {
return len(runes)
}
return len(chars.bytes)
return len(chars.slice)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int {
func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown {
return chars.trimLength
}
chars.trimLengthKnown = true
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
@@ -80,7 +120,8 @@ func (chars *Chars) TrimLength() int {
break
}
}
return i - j + 1
chars.trimLength = AsUint16(i - j + 1)
return chars.trimLength
}
func (chars *Chars) TrailingWhitespaces() int {
@@ -96,62 +137,31 @@ func (chars *Chars) TrailingWhitespaces() int {
}
func (chars *Chars) ToString() string {
if chars.runes != nil {
return string(chars.runes)
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
}
return string(chars.bytes)
return string(chars.slice)
}
func (chars *Chars) ToRunes() []rune {
if chars.runes != nil {
return chars.runes
if runes := chars.optionalRunes(); runes != nil {
return runes
}
runes := make([]rune, len(chars.bytes))
for idx, b := range chars.bytes {
bytes := chars.slice
runes := make([]rune, len(bytes))
for idx, b := range bytes {
runes[idx] = rune(b)
}
return runes
}
func (chars *Chars) Slice(b int, e int) Chars {
if chars.runes != nil {
return Chars{runes: chars.runes[b:e]}
func (chars *Chars) CopyRunes(dest []rune) {
if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes)
return
}
return Chars{bytes: chars.bytes[b:e]}
}
func (chars *Chars) Split(delimiter string) []Chars {
delim := []rune(delimiter)
numChars := chars.Length()
numDelim := len(delim)
begin := 0
ret := make([]Chars, 0, 1)
for index := 0; index < numChars; {
if index+numDelim <= numChars {
match := true
for off, d := range delim {
if chars.Get(index+off) != d {
match = false
break
}
}
// Found the delimiter
if match {
incr := Max(numDelim, 1)
ret = append(ret, chars.Slice(begin, index+incr))
index += incr
begin = index
continue
}
} else {
// Impossible to find the delimiter in the remaining substring
break
}
index++
}
if begin < numChars || len(ret) == 0 {
ret = append(ret, chars.Slice(begin, numChars))
}
return ret
for idx, b := range chars.slice {
dest[idx] = rune(b)
}
return
}

View File

@@ -2,27 +2,16 @@ package util
import "testing"
func TestToCharsNil(t *testing.T) {
bs := Chars{bytes: []byte{}}
if bs.bytes == nil || bs.runes != nil {
t.Error()
}
rs := RunesToChars([]rune{})
if rs.bytes != nil || rs.runes == nil {
t.Error()
}
}
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
if chars.ToString() != "foobar" || chars.runes != nil {
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 {
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp int) {
check := func(str string, exp uint16) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
@@ -55,28 +44,3 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5)
check(" ", 0)
}
func TestSplit(t *testing.T) {
check := func(str string, delim string, tokens ...string) {
input := ToChars([]byte(str))
result := input.Split(delim)
if len(result) != len(tokens) {
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
str, len(result), len(tokens), result)
}
for idx, token := range tokens {
if result[idx].ToString() != token {
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
str, result[idx].ToString(), token)
}
}
}
check("abc:def::", ":", "abc:", "def:", ":")
check("abc:def::", "-", "abc:def::")
check("abc", "", "a", "b", "c")
check("abc", "a", "a", "bc")
check("abc", "ab", "ab", "c")
check("abc", "abc", "abc")
check("abc", "abcd", "abc")
check("", "abcd", "")
}

View File

@@ -26,23 +26,23 @@ func NewEventBox() *EventBox {
// Wait blocks the goroutine until signaled
func (b *EventBox) Wait(callback func(*Events)) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
if len(b.events) == 0 {
b.cond.Wait()
}
callback(&b.events)
b.cond.L.Unlock()
}
// Set turns on the event type on the box
func (b *EventBox) Set(event EventType, value interface{}) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
b.events[event] = value
if _, found := b.ignore[event]; !found {
b.cond.Broadcast()
}
b.cond.L.Unlock()
}
// Clear clears the events
@@ -56,27 +56,27 @@ func (events *Events) Clear() {
// Peek peeks at the event box if the given event is set
func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock()
defer b.cond.L.Unlock()
_, ok := b.events[event]
b.cond.L.Unlock()
return ok
}
// Watch deletes the events from the ignore list
func (b *EventBox) Watch(events ...EventType) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
for _, event := range events {
delete(b.ignore, event)
}
b.cond.L.Unlock()
}
// Unwatch adds the events to the ignore list
func (b *EventBox) Unwatch(events ...EventType) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
for _, event := range events {
b.ignore[event] = true
}
b.cond.L.Unlock()
}
// WaitFor blocks the execution until the event is received

View File

@@ -163,7 +163,7 @@ Execute (fzf#shellescape with cmd.exe):
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
" AssertEqual '^"%%USERPROFILE%%^", fzf#shellescape('%USERPROFILE%', 'cmd.exe')
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
Execute (Cleanup):
unlet g:dir

View File

@@ -73,6 +73,7 @@ class Tmux
else
raise "Unknown shell: #{shell}"
end
go("set-window-option -t #{@win} pane-base-index 0")
@lines = `tput lines`.chomp.to_i
if shell == :fish
@@ -259,6 +260,12 @@ class TestGoFZF < TestBase
assert_equal 'hello', readonce.chomp
end
def test_fzf_default_command_failure
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
tmux.until { |lines| lines[-2].include?('ERROR') }
tmux.send_keys :Enter
end
def test_key_bindings
tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
@@ -1246,11 +1253,12 @@ class TestGoFZF < TestBase
end
def test_no_clear
tmux.send_keys 'seq 100 | fzf --no-clear --inline-info --height 5', :Enter
prompt = '> < 100/100'
tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter
prompt = '> < 10/10'
tmux.until { |lines| lines[-1] == prompt }
tmux.send_keys :Enter
tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' }
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
tmux.until { |lines| lines[-1] == prompt }
end
def test_change_top
@@ -1266,6 +1274,16 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-3] == '> 11' }
tmux.send_keys :Enter
end
def test_preview_update_on_select
tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter)
tmux.until { |lines| lines.item_count == 10 }
tmux.send_keys 'a'
tmux.until { |lines| lines.any? { |line| line.include? '1 2 3 4 5' } }
tmux.send_keys 'a'
tmux.until { |lines| !lines.any? { |line| line.include? '1 2 3 4 5' } }
end
end
module TestShell