Compare commits

...

49 Commits

Author SHA1 Message Date
Junegunn Choi
c656cfbdce Update doc 2015-09-12 13:31:07 +09:00
Junegunn Choi
de829c0938 0.10.5 2015-09-12 12:50:32 +09:00
Junegunn Choi
64443221aa Fix #344 - Backward scan when --tiebreak=end 2015-09-12 11:37:55 +09:00
Junegunn Choi
9017e29741 Make it possible to unquote the term in extended-exact mode
Close #338
2015-09-12 11:00:30 +09:00
Junegunn Choi
0a22142d88 [fzf-tmux] Fix #343 - Escape backticks in --query 2015-09-07 18:40:39 +09:00
Junegunn Choi
ac160f98a8 [gvim] Fix #342 - Should not escape launcher part of the command 2015-09-05 21:39:12 +09:00
Junegunn Choi
62e01a2a62 [vim] Escape newline character when running fzf with :!
Fixes Helptags! command from fzf.vim
2015-09-01 01:13:35 +09:00
Junegunn Choi
5660cebaf6 [zsh-completion] Temporarily unset shwordsplit (#328) 2015-09-01 00:51:28 +09:00
Junegunn Choi
a7e588ceac Merge pull request #336 from fazibear/fix-fish-streams
Fix CTRL-T on fish to work asynchronously
2015-08-30 21:21:13 +09:00
Michał Kalbarczyk
5baf1c5536 fix fish streams 2015-08-30 14:05:24 +02:00
Junegunn Choi
9a2d9ad947 0.10.4 2015-08-29 02:36:27 +09:00
Junegunn Choi
90b0cd44ac Should not strip ANSI codes when --ansi is not set 2015-08-28 21:23:10 +09:00
Junegunn Choi
698e8008df [vim] Dynamic height specification for 'up' and 'down' options
Values for 'up' and 'down' can be written with ~ prefix. Only applies
when the source is a Vim list.

    e.g. { 'source': range(10), 'down': '~40%' }
2015-08-28 18:38:47 +09:00
Junegunn Choi
1de4cc3ba8 [install] Fall back statically-linked binary on 64-bit linux
Close #322
2015-08-27 22:50:59 +09:00
Junegunn Choi
0d66ad23c6 Fix build script 2015-08-27 22:48:42 +09:00
Junegunn Choi
7f7741099b make linux-static (#322) 2015-08-27 03:28:05 +09:00
Junegunn Choi
5a72dc6922 Fix #329 - Trim ANSI codes from output when --ansi & --with-nth are set 2015-08-26 23:58:18 +09:00
Junegunn Choi
80ed02e72e Add failing test case for #329 2015-08-26 23:35:31 +09:00
Junegunn Choi
8fb31e1b4d [vim] Escape % and # when running source command with :! 2015-08-24 01:52:16 +09:00
Junegunn Choi
148f21415a Mention fzf.vim project 2015-08-22 19:33:04 +09:00
Junegunn Choi
1c31e07d34 [install] Improve error message 2015-08-19 19:42:06 +09:00
Junegunn Choi
55d566b72f Revert "[vim] Open silently"
This reverts commit c601fc6437.
2015-08-18 12:03:08 +09:00
Junegunn Choi
60336c7423 Remove Vim examples from README.md 2015-08-16 02:47:52 +09:00
Junegunn Choi
7ae877bd3a [vim] Handle single/double quote characters in 'dir' option 2015-08-16 00:04:45 +09:00
Junegunn Choi
c601fc6437 [vim] Open silently 2015-08-15 23:53:27 +09:00
Junegunn Choi
e5fec408c4 [vim] tab split instead of tabedit 2015-08-15 23:53:11 +09:00
Junegunn Choi
8156e9894e 0.10.3 2015-08-12 02:09:46 +09:00
Junegunn Choi
cacc212f12 [install] Prerelease of 0.10.3 2015-08-11 00:21:09 +09:00
Junegunn Choi
d0f2c00f9f Fix --with-nth performance; use simpler regular expression
Related #317
2015-08-11 00:15:41 +09:00
Junegunn Choi
766427de0c Fix --with-nth performance; avoid regex if possible
Close #317
2015-08-10 18:34:20 +09:00
Junegunn Choi
a7b75c99a5 [install] Stop installer when failed to download the binary
Close #312
2015-08-08 03:53:46 +09:00
Junegunn Choi
bae10a6582 [install] Add an extra new line character
so that it doesn't corrupt file that doesn't end with a new line
character. Close #311.
2015-08-05 23:50:38 +09:00
Junegunn Choi
c4cf90a3d2 0.10.2 2015-08-03 00:21:21 +09:00
Junegunn Choi
15c49a3e08 Fix race condition 2015-08-03 00:14:34 +09:00
Junegunn Choi
ae87f6548a GoLint 2015-08-02 23:54:53 +09:00
Junegunn Choi
7833fa7396 [install] Always download binary when --pre is set 2015-08-02 15:09:57 +09:00
Junegunn Choi
9278f3acd2 [install] Add --pre option for downloading prerelease binary 2015-08-02 15:02:12 +09:00
Junegunn Choi
e83ae34a3b Update CHANGELOG - 0.10.2 2015-08-02 14:32:34 +09:00
Junegunn Choi
e13bafc1ab Performance fix - unnecessary rune convertion on --ansi
> time cat /tmp/list | fzf-0.10.1-darwin_amd64 --ansi -fqwerty > /dev/null

    real    0m4.364s
    user    0m8.231s
    sys     0m0.820s

    > time cat /tmp/list | fzf --ansi -fqwerty > /dev/null

    real    0m4.624s
    user    0m5.755s
    sys     0m0.732s
2015-08-02 14:25:57 +09:00
Junegunn Choi
0ea66329b8 Performance tuning - eager rune array conversion
> wc -l /tmp/list2
     2594098 /tmp/list2

    > time cat /tmp/list2 | fzf-0.10.1-darwin_amd64 -fqwerty > /dev/null

    real    0m5.418s
    user    0m10.990s
    sys     0m1.302s

    > time cat /tmp/list2 | fzf-head -fqwerty > /dev/null

    real    0m4.862s
    user    0m6.619s
    sys     0m0.982s
2015-08-02 14:00:18 +09:00
Junegunn Choi
634670e3ea Lint 2015-08-02 13:11:59 +09:00
Junegunn Choi
dea60b11bc Only consider the lengths of the relevant parts when --nth is set 2015-08-01 23:13:24 +09:00
Junegunn Choi
5e90f0a57b Fix default command so that it doesn't fail on dash-prefixed files
Close #310
2015-08-01 21:51:10 +09:00
Junegunn Choi
0b4542fcdf [vim] Temporarily disable &autochdir when opening files (#306) 2015-07-29 17:55:58 +09:00
Junegunn Choi
02bd2d2adf Do not proceed if $TERM is invalid
Related #305
2015-07-28 14:35:46 +09:00
Junegunn Choi
dce6fe6f2d [fzf-tmux] Ensure that the same $TERM value is used in split
Fix #305. ncurses can crash on invalid $TERM. fzf-tmux uses bash on
a new pane so we have to make sure that the $TERM is consistent with
that of the hosting shell.
2015-07-28 14:17:25 +09:00
Junegunn Choi
fcae99f09b No need to "tmux list-panes" when obviously not on tmux (#303) 2015-07-28 00:56:03 +09:00
Junegunn Choi
fb1b026d3d Always check if the pane is zoomed
Close #303
2015-07-28 00:30:17 +09:00
Junegunn Choi
9f953fc944 Do not use tmux pane if the current pane is zoomed
Close #303
2015-07-28 00:22:04 +09:00
37 changed files with 689 additions and 400 deletions

View File

@@ -1,6 +1,39 @@
CHANGELOG CHANGELOG
========= =========
0.10.5
------
- `'`-prefix to unquote the term in `--extended-exact` mode
- Backward scan when `--tiebreak=end` is set
0.10.4
------
- Fixed to remove ANSI code from output when `--with-nth` is set
0.10.3
------
- Fixed slow performance of `--with-nth` when used with `--delimiter`
- Regular expression engine of Golang as of now is very slow, so the fixed
version will treat the given delimiter pattern as a plain string instead
of a regular expression unless it contains special characters and is
a valid regular expression.
- Simpler regular expression for delimiter for better performance
0.10.2
------
### Fixes and improvements
- Improvement in perceived response time of queries
- Eager, efficient rune array conversion
- Graceful exit when failed to initialize ncurses (invalid $TERM)
- Improved ranking algorithm when `--nth` option is set
- Changed the default command not to fail when there are files whose names
start with dash
0.10.1 0.10.1
------ ------

View File

@@ -124,8 +124,9 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) | | `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match | | `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't need fuzzy matching and do not wish to "quote" every word, start If you don't prefer fuzzy matching and do not wish to "quote" every word,
fzf with `-e` or `--extended-exact` option. start fzf with `-e` or `--extended-exact` option. Note that in
`--extended-exact` mode, `'`-prefix "unquotes" the term.
#### Environment variables #### Environment variables
@@ -254,6 +255,9 @@ export FZF_COMPLETION_OPTS='+c -x'
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
This repository only enables basic integration with Vim. If you're looking for
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.) (Note: To use fzf in GVim, an external terminal emulator is required.)
#### `:FZF[!]` #### `:FZF[!]`
@@ -286,10 +290,8 @@ customization.
#### `fzf#run([options])` #### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list For more advanced uses, you can use `fzf#run()` function with the following
of the selected items. options.
`fzf#run()` may take an options-dictionary:
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -305,65 +307,7 @@ of the selected items.
| `launcher` | string | External terminal emulator to start fzf with (GVim only) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you Examples can be found on [the wiki
should use `sink` or `sink*` to process the output from fzf._
##### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
```vim
let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
But if `sink` is given as a string, the command will be executed for each
selected item.
```vim
" Each selected item will be opened in a new tab
let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
We can also use a Vim list as the source as follows:
```vim
" Choose a color scheme with fzf
nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
`sink` option can be a function reference. The following example creates a
handy mapping that selects an open buffer.
```vim
" List of buffers
function! s:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! s:bufopen(e)
execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(<sid>buflist()),
\ 'sink': function('<sid>bufopen'),
\ 'options': '+m',
\ 'down': len(<sid>buflist()) + 2
\ })<CR>
```
More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
Tips Tips
@@ -425,14 +369,6 @@ of fzf to a temporary file.
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
``` ```
#### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
```sh
find . | iconv -f utf-8-mac -t utf8//ignore | fzf
```
License License
------- -------

View File

@@ -82,7 +82,7 @@ while [ $# -gt 0 ]; do
shift shift
done done
if [ -z "$TMUX_PANE" ]; then if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then
fzf "${args[@]}" fzf "${args[@]}"
exit $? exit $?
fi fi
@@ -107,7 +107,7 @@ fail() {
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found" [ -x "$fzf" ] || fail "fzf executable not found"
envs="env " envs="env TERM=$TERM "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
@@ -117,7 +117,9 @@ mkfifo $fifo3
# Build arguments to fzf # Build arguments to fzf
opts="" opts=""
for arg in "${args[@]}"; do for arg in "${args[@]}"; do
opts="$opts \"${arg//\"/\\\"}\"" arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}"
opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then

43
install
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.10.1 [[ "$@" =~ --pre ]] && version=0.10.5 pre=1 ||
version=0.10.5 pre=0
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -20,16 +21,21 @@ ask() {
check_binary() { check_binary() {
echo -n " - Checking fzf executable ... " echo -n " - Checking fzf executable ... "
local output=$("$fzf_base"/bin/fzf --version 2>&1) local output
if [ "$version" = "$output" ]; then output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
elif [ "$version" != "$output" ]; then
echo "$output != $version"
binary_error="Invalid version"
else
echo "$output" echo "$output"
binary_error="" binary_error=""
else return 0
echo "$output != $version"
rm -f "$fzf_base"/bin/fzf
binary_error="Invalid binary"
return 1
fi fi
rm -f "$fzf_base"/bin/fzf
return 1
} }
symlink() { symlink() {
@@ -45,11 +51,13 @@ symlink() {
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then if [ $pre = 0 ]; then
echo " - Already exists" if [ -x "$fzf_base"/bin/fzf ]; then
check_binary && return echo " - Already exists"
elif [ -x "$fzf_base"/bin/$1 ]; then check_binary && return
symlink $1 && check_binary && return elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -72,7 +80,12 @@ download() {
return return
fi fi
chmod +x $1 && symlink $1 && check_binary chmod +x $1 && symlink $1 || return 1
if [[ $1 =~ linux_amd64$ ]]; then
check_binary || download $1-static
else
check_binary
fi
} }
# Try to download binary executable # Try to download binary executable
@@ -93,6 +106,7 @@ if [ -n "$binary_error" ]; then
echo "No prebuilt binary for $archi ... " echo "No prebuilt binary for $archi ... "
else else
echo " - $binary_error !!!" echo " - $binary_error !!!"
exit 1
fi fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
@@ -247,6 +261,7 @@ append_line() {
if [ -n "$line" ]; then if [ -n "$line" ]; then
echo " - Already exists: line #$line" echo " - Already exists: line #$line"
else else
echo >> "$2"
echo "$1" >> "$2" echo "$1" >> "$2"
echo " + Added" echo " + Added"
fi fi

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -374,29 +374,40 @@ mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted) .SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') is interpreted as an A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
of the string. occurrences of the string.
.SS Anchored-match .SS Anchored-match
A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
Then fzf will search for the items that start with or end with the given anchored-match term. Then fzf will search for the items that start with or end
string. An anchored-match term is also an exact-match term. with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by !, fzf will exclude the items that satisfy the term If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
from the result. term from the result.
.SS Extended-exact mode .SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option \fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fB-x\fR or \fB--extended\fR). (instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR
mode, \fB'\fR-prefix "unquotes" the term.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO .SH SEE ALSO
.B Project homepage:
.RS
.I https://github.com/junegunn/fzf .I https://github.com/junegunn/fzf
.RE
.br
.R ""
.br
.B Extra Vim plugin:
.RS
.I https://github.com/junegunn/fzf.vim
.RE
.SH LICENSE .SH LICENSE
MIT MIT

View File

@@ -54,13 +54,17 @@ function! s:fzf_exec()
return s:exec return s:exec
endfunction endfunction
function! s:tmux_not_zoomed()
return system('tmux list-panes -F "#F"') !~# 'Z'
endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
if has('gui_running') if has('gui_running')
return 0 return 0
endif endif
if exists('s:tmux') if exists('s:tmux')
return s:tmux return s:tmux && s:tmux_not_zoomed()
endif endif
let s:tmux = 0 let s:tmux = 0
@@ -68,7 +72,7 @@ function! s:tmux_enabled()
let output = system('tmux -V') let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7' let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif endif
return s:tmux return s:tmux && s:tmux_not_zoomed()
endfunction endfunction
function! s:shellesc(arg) function! s:shellesc(arg)
@@ -76,7 +80,7 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return escape(a:path, ' %#\') return escape(a:path, ' %#''"\')
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -160,7 +164,13 @@ function! s:fzf_tmux(dict)
let size = '' let size = ''
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : spec)
endif
break break
endif endif
endfor endfor
@@ -207,12 +217,13 @@ let s:launcher = function('s:xterm_launcher')
function! s:execute(dict, command, temps) function! s:execute(dict, command, temps)
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running') if has('gui_running')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
else else
let command = a:command let command = escaped
endif endif
execute 'silent !'.command execute 'silent !'.command
redraw! redraw!
@@ -240,12 +251,21 @@ function! s:execute_tmux(dict, command, temps)
return s:callback(a:dict, a:temps) return s:callback(a:dict, a:temps)
endfunction endfunction
function! s:calc_size(max, val) function! s:calc_size(max, val, dict)
if a:val =~ '%$' if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100 let size = a:max * str2nr(a:val[:-2]) / 100
else else
return min([a:max, a:val]) let size = min([a:max, str2nr(a:val)])
endif endif
let srcsz = -1
if type(get(a:dict, 'source', 0)) == type([])
let srcsz = len(a:dict.source)
endif
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
function! s:getpos() function! s:getpos()
@@ -264,7 +284,11 @@ function! s:split(dict)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
if !empty(val) if !empty(val)
let [cmd, resz, max] = triple let [cmd, resz, max] = triple
let sz = s:calc_size(max, val) if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val[1:], a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new' execute cmd sz.'new'
execute resz sz execute resz sz
return return
@@ -354,7 +378,7 @@ endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e', \ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
@@ -364,9 +388,15 @@ function! s:cmd_callback(lines) abort
endif endif
let key = remove(a:lines, 0) let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e') let cmd = get(s:action, key, 'e')
for item in a:lines try
execute cmd s:escape(item) let autochdir = &autochdir
endfor set noautochdir
for item in a:lines
execute cmd s:escape(item)
endfor
finally
let &autochdir = autochdir
endtry
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
@@ -374,7 +404,7 @@ function! s:cmd(bang, ...) abort
let args = extend(['--expect='.join(keys(s:action), ',')], a:000) let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {} let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1])) if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1) let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif endif
if !a:bang if !a:bang
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))

View File

@@ -99,7 +99,11 @@ EOF
} }
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws
if setopt | grep shwordsplit > /dev/null; then
sws=1
unsetopt shwordsplit
fi
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -148,6 +152,7 @@ fzf-completion() {
else else
eval "zle ${fzf_default_completion:-expand-or-complete}" eval "zle ${fzf_default_completion:-expand-or-complete}"
fi fi
[ -n "$sws" ] && setopt shwordsplit
} }
[ -z "$fzf_default_completion" ] && [ -z "$fzf_default_completion" ] &&

View File

@@ -19,7 +19,7 @@ function fzf_key_bindings
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result"
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -2,6 +2,7 @@ FROM base/archlinux:2014.07.03
MAINTAINER Junegunn Choi <junegunn.c@gmail.com> MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4 # Install Go 1.4

View File

@@ -3,7 +3,7 @@ MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN apt-get update && apt-get -y upgrade && \ RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential libncurses-dev apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \

View File

@@ -5,8 +5,19 @@ endif
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
GOOS := darwin GOOS := darwin
LDFLAGS :=
ifdef STATIC
$(error Static linking not possible on OS X)
endif
else ifeq ($(UNAME_S),Linux) else ifeq ($(UNAME_S),Linux)
GOOS := linux GOOS := linux
ifdef STATIC
SUFFIX := -static
LDFLAGS := --ldflags '-extldflags "-static -ltinfo -lgpm"'
else
SUFFIX :=
LDFLAGS :=
endif
endif endif
ifneq ($(shell uname -m),x86_64) ifneq ($(shell uname -m),x86_64)
@@ -16,21 +27,24 @@ endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
BINDIR := ../bin BINDIR := ../bin
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386$(SUFFIX)
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX)
VERSION = $(shell fzf/$(BINARY64) --version) VERSION = $(shell fzf/$(BINARY64) --version)
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX)
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX)
all: release all: release
release: build release: build
cd fzf && \ -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64)
rm $(RELEASE32) $(RELEASE64)
ifndef STATIC
build: test fzf/$(BINARY32) fzf/$(BINARY64) build: test fzf/$(BINARY32) fzf/$(BINARY64)
else
build: test fzf/$(BINARY64)
endif
test: test:
go get go get
@@ -42,13 +56,13 @@ uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
clean: clean:
cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz cd fzf && rm -f fzf-*
fzf/$(BINARY32): $(SOURCES) fzf/$(BINARY32): $(SOURCES)
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32)
fzf/$(BINARY64): $(SOURCES) fzf/$(BINARY64): $(SOURCES)
cd fzf && go build -o $(BINARY64) cd fzf && go build $(LDFLAGS) -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR)
@@ -57,18 +71,27 @@ $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
# Linux distribution to build fzf on docker-arch:
DISTRO := arch docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker: docker-ubuntu:
docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
linux: docker arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
$(DISTRO): docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
.PHONY: all build release test install uninstall clean docker linux $(DISTRO) ubuntu: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
linux: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/arch-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
linux-static: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make STATIC=1'
.PHONY: all build release test install uninstall clean docker \
linux linux-static arch ubuntu docker-arch docker-ubuntu

View File

@@ -15,8 +15,15 @@ import (
* In short: They try to do as little work as possible. * In short: They try to do as little work as possible.
*/ */
func runeAt(runes []rune, index int, max int, forward bool) rune {
if forward {
return runes[index]
}
return runes[max-index-1]
}
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
@@ -34,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
sidx := -1 sidx := -1
eidx := -1 eidx := -1
for index, char := range *runes { lenRunes := len(runes)
lenPattern := len(pattern)
for index := range runes {
char := runeAt(runes, index, lenRunes, forward)
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string
if !caseSensitive { if !caseSensitive {
@@ -47,11 +58,12 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if char == pattern[pidx] { pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
} }
if pidx++; pidx == len(pattern) { if pidx++; pidx == lenPattern {
eidx = index + 1 eidx = index + 1
break break
} }
@@ -61,7 +73,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 { if sidx >= 0 && eidx >= 0 {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := (*runes)[index] char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -69,14 +81,19 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if char == pattern[pidx] {
pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
break break
} }
} }
} }
return sidx, eidx if forward {
return sidx, eidx
}
return lenRunes - eidx, lenRunes - sidx
} }
return -1, -1 return -1, -1
} }
@@ -88,20 +105,21 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
// //
// We might try to implement better algorithms in the future: // We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm // http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
numRunes := len(*runes) lenRunes := len(runes)
plen := len(pattern) lenPattern := len(pattern)
if numRunes < plen {
if lenRunes < lenPattern {
return -1, -1 return -1, -1
} }
pidx := 0 pidx := 0
for index := 0; index < numRunes; index++ { for index := 0; index < lenRunes; index++ {
char := (*runes)[index] char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -109,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if pattern[pidx] == char { pchar := runeAt(pattern, pidx, lenPattern, forward)
if pchar == char {
pidx++ pidx++
if pidx == plen { if pidx == lenPattern {
return index - plen + 1, index + 1 if forward {
return index - lenPattern + 1, index + 1
}
return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
} }
} else { } else {
index -= pidx index -= pidx
@@ -123,13 +145,13 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) { if len(runes) < len(pattern) {
return -1, -1 return -1, -1
} }
for index, r := range pattern { for index, r := range pattern {
char := (*runes)[index] char := runes[index]
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -141,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) {
runes := util.TrimRight(input) runes := util.TrimRight(input)
trimmedLen := len(runes) trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
@@ -161,11 +183,12 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
return trimmedLen - len(pattern), trimmedLen return trimmedLen - len(pattern), trimmedLen
} }
func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { // EqualMatch performs equal-match
if len(*runes) != len(pattern) { func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) != len(pattern) {
return -1, -1 return -1, -1
} }
runesStr := string(*runes) runesStr := string(runes)
if !caseSensitive { if !caseSensitive {
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }

View File

@@ -5,12 +5,11 @@ import (
"testing" "testing"
) )
func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
runes := []rune(input) s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern))
s, e := fun(caseSensitive, &runes, []rune(pattern))
if s != sidx { if s != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
} }
@@ -20,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseS
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1)
}
func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4)
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5)
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1)
}
}
func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3)
assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3)
assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1)
}
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1)
}
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6)
}
} }

View File

@@ -36,7 +36,7 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
} }
func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiState) { func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
@@ -45,9 +45,9 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
} }
idx := 0 idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
output.WriteString((*str)[idx:offset[0]]) output.WriteString(str[idx:offset[0]])
newState := interpretCode((*str)[offset[0]:offset[1]], state) newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
@@ -69,7 +69,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
idx = offset[1] idx = offset[1]
} }
rest := (*str)[idx:] rest := str[idx:]
if len(rest) > 0 { if len(rest) > 0 {
output.WriteString(rest) output.WriteString(rest)
if state != nil { if state != nil {
@@ -77,8 +77,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
} }
} }
outputStr := output.String() return output.String(), offsets, state
return &outputStr, offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -17,9 +17,9 @@ func TestExtractColor(t *testing.T) {
var state *ansiState var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
output, ansiOffsets, newState := extractColor(&src, state) output, ansiOffsets, newState := extractColor(src, state)
state = newState state = newState
if *output != "hello world" { if output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: {}", output)
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)

View File

@@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a // ItemBuilder is a closure type that builds Item object from a pointer to a
// string and an integer // string and an integer
type ItemBuilder func(*string, int) *Item type ItemBuilder func([]byte, int) *Item
// ChunkList is a list of Chunks // ChunkList is a list of Chunks
type ChunkList struct { type ChunkList struct {
@@ -26,7 +26,7 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool { func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
item := trans(data, index) item := trans(data, index)
if item != nil { if item != nil {
*c = append(*c, item) *c = append(*c, item)
@@ -53,7 +53,7 @@ func CountItems(cs []*Chunk) int {
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data string) bool { func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock() defer cl.mutex.Unlock()
@@ -62,7 +62,7 @@ func (cl *ChunkList) Push(data string) bool {
cl.chunks = append(cl.chunks, &newChunk) cl.chunks = append(cl.chunks, &newChunk)
} }
if cl.lastChunk().push(cl.trans, &data, cl.count) { if cl.lastChunk().push(cl.trans, data, cl.count) {
cl.count++ cl.count++
return true return true
} }

View File

@@ -6,8 +6,8 @@ import (
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
cl := NewChunkList(func(s *string, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
}) })
// Snapshot // Snapshot
@@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) {
} }
// Add some data // Add some data
cl.Push("hello") cl.Push([]byte("hello"))
cl.Push("world") cl.Push([]byte("world"))
// Previously created snapshot should remain the same // Previously created snapshot should remain the same
if len(snapshot) > 0 { if len(snapshot) > 0 {
@@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 { if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 || if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
*(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 { string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {
@@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) {
// Add more data // Add more data
for i := 0; i < chunkSize*2; i++ { for i := 0; i < chunkSize*2; i++ {
cl.Push(fmt.Sprintf("item %d", i)) cl.Push([]byte(fmt.Sprintf("item %d", i)))
} }
// Previous snapshot should remain the same // Previous snapshot should remain the same
@@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items") t.Error("Unexpected number of items")
} }
cl.Push("hello") cl.Push([]byte("hello"))
cl.Push("world") cl.Push([]byte("world"))
lastChunkCount := len(*snapshot[len(snapshot)-1]) lastChunkCount := len(*snapshot[len(snapshot)-1])
if lastChunkCount != 2 { if lastChunkCount != 2 {

View File

@@ -8,14 +8,14 @@ import (
const ( const (
// Current version // Current version
Version = "0.10.1" version = "0.10.5"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader // Reader
defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
// Terminal // Terminal
initialDelay = 100 * time.Millisecond initialDelay = 100 * time.Millisecond

View File

@@ -55,7 +55,7 @@ func Run(opts *Options) {
rankTiebreak = opts.Tiebreak rankTiebreak = opts.Tiebreak
if opts.Version { if opts.Version {
fmt.Println(Version) fmt.Println(version)
os.Exit(0) os.Exit(0)
} }
@@ -63,62 +63,68 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) { ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
// By default, we do nothing return util.BytesToRunes(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil return data, nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, offsets, newState := extractColor(data, state) trimmed, offsets, newState := extractColor(string(data), state)
state = newState state = newState
return trimmed, offsets return []rune(trimmed), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, _, _ := extractColor(data, nil) trimmed, _, _ := extractColor(string(data), nil)
return trimmed, nil return []rune(trimmed), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
} }
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, *data) header = append(header, string(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
data, colors := ansiProcessor(data) runes, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: runes,
index: uint32(index), index: uint32(index),
colors: colors, colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, *joinTokens(trans)) header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: data, origText: &runes,
index: uint32(index), index: uint32(index),
colors: nil, colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text) trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@@ -128,8 +134,8 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) bool { reader := Reader{func(data []byte) bool {
return chunkList.Push(str) return chunkList.Push(data)
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
} }
@@ -137,7 +143,8 @@ func Run(opts *Options) {
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) opts.Mode, opts.Case, opts.Tiebreak != byEnd,
opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -151,10 +158,10 @@ func Run(opts *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(str string) bool { func(runes []byte) bool {
item := chunkList.trans(&str, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(string(item.text))
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
@@ -168,7 +175,7 @@ func Run(opts *Options) {
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) fmt.Println(merger.Get(i).AsString(opts.Ansi))
} }
} }
os.Exit(0) os.Exit(0)
@@ -244,7 +251,7 @@ func Run(opts *Options) {
fmt.Println() fmt.Println()
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString()) fmt.Println(val.Get(i).AsString(opts.Ansi))
} }
os.Exit(0) os.Exit(0)
} }

View File

@@ -8,6 +8,7 @@ package curses
import "C" import "C"
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -258,6 +259,10 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.newterm(nil, C.stderr, C.stdin) _screen = C.newterm(nil, C.stderr, C.stdin)
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(1)
}
C.set_term(_screen) C.set_term(_screen)
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
) )
// History struct represents input history
type History struct { type History struct {
path string path string
lines []string lines []string
@@ -15,6 +16,7 @@ type History struct {
cursor int cursor int
} }
// NewHistory returns the pointer to a new History struct
func NewHistory(path string, maxSize int) (*History, error) { func NewHistory(path string, maxSize int) (*History, error) {
fmtError := func(e error) error { fmtError := func(e error) error {
if os.IsPermission(e) { if os.IsPermission(e) {

View File

@@ -17,9 +17,9 @@ type colorOffset struct {
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text *string text []rune
origText *string origText *[]rune
transformed *[]Token transformed []Token
index uint32 index uint32
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
@@ -37,14 +37,14 @@ type Rank struct {
var rankTiebreak tiebreak var rankTiebreak tiebreak
// Rank calculates rank of the Item // Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank { func (item *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) { if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
return i.rank return item.rank
} }
matchlen := 0 matchlen := 0
prevEnd := 0 prevEnd := 0
minBegin := math.MaxUint16 minBegin := math.MaxUint16
for _, offset := range i.offsets { for _, offset := range item.offsets {
begin := int(offset[0]) begin := int(offset[0])
end := int(offset[1]) end := int(offset[1])
if prevEnd > begin { if prevEnd > begin {
@@ -63,13 +63,22 @@ func (i *Item) Rank(cache bool) Rank {
var tiebreak uint16 var tiebreak uint16
switch rankTiebreak { switch rankTiebreak {
case byLength: case byLength:
tiebreak = uint16(len(*i.text)) // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
lenSum := 0
for _, token := range item.transformed {
lenSum += len(token.text)
}
tiebreak = uint16(lenSum)
} else {
tiebreak = uint16(len(item.text))
}
case byBegin: case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term // We can't just look at item.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin) tiebreak = uint16(minBegin)
case byEnd: case byEnd:
if prevEnd > 0 { if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd) tiebreak = uint16(1 + len(item.text) - prevEnd)
} else { } else {
// Empty offsets due to inverse terms. // Empty offsets due to inverse terms.
tiebreak = 1 tiebreak = 1
@@ -77,24 +86,30 @@ func (i *Item) Rank(cache bool) Rank {
case byIndex: case byIndex:
tiebreak = 1 tiebreak = 1
} }
rank := Rank{uint16(matchlen), tiebreak, i.index} rank := Rank{uint16(matchlen), tiebreak, item.index}
if cache { if cache {
i.rank = rank item.rank = rank
} }
return rank return rank
} }
// AsString returns the original string // AsString returns the original string
func (i *Item) AsString() string { func (item *Item) AsString(stripAnsi bool) string {
return *i.StringPtr() return *item.StringPtr(stripAnsi)
} }
// StringPtr returns the pointer to the original string // StringPtr returns the pointer to the original string
func (i *Item) StringPtr() *string { func (item *Item) StringPtr(stripAnsi bool) *string {
if i.origText != nil { if item.origText != nil {
return i.origText if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil)
return &trimmed
}
orig := string(*item.origText)
return &orig
} }
return i.text str := string(item.text)
return &str
} }
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {

View File

@@ -39,14 +39,14 @@ func TestRankComparison(t *testing.T) {
// Match length, string length, index // Match length, string length, index
func TestItemRank(t *testing.T) { func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} item2 := Item{text: strs[0], index: 0, offsets: []Offset{}}
items := []*Item{&item1, &item2} items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
@@ -62,10 +62,10 @@ func TestItemRank(t *testing.T) {
} }
// Sort by relevance // Sort by relevance
item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 || if items[0] != &item2 || items[1] != &item1 ||

View File

@@ -96,7 +96,7 @@ func (m *Matcher) Loop() {
} }
if !cancelled { if !cancelled {
if merger.Cacheable() { if merger.cacheable() {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = merger
} }
merger.final = request.final merger.final = request.final

View File

@@ -82,7 +82,7 @@ func (mg *Merger) Get(idx int) *Item {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
} }
func (mg *Merger) Cacheable() bool { func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }

View File

@@ -22,7 +22,7 @@ func randItem() *Item {
offsets[idx] = Offset{sidx, eidx} offsets[idx] = Offset{sidx, eidx}
} }
return &Item{ return &Item{
text: &str, text: []rune(str),
index: rand.Uint32(), index: rand.Uint32(),
offsets: offsets} offsets: offsets}
} }

View File

@@ -1,7 +1,6 @@
package fzf package fzf
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
@@ -104,7 +103,7 @@ type Options struct {
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter Delimiter
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak Tiebreak tiebreak
@@ -149,7 +148,7 @@ func defaultOptions() *Options {
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength, Tiebreak: byLength,
@@ -268,17 +267,23 @@ func splitNth(str string) []Range {
return ranges return ranges
} }
func delimiterRegexp(str string) *regexp.Regexp { func delimiterRegexp(str string) Delimiter {
rx, e := regexp.Compile(str) // Special handling of \t
if e != nil { str = strings.Replace(str, "\\t", "\t", -1)
str = regexp.QuoteMeta(str)
// 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
} }
rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) rx, e := regexp.Compile(str)
// 2. Pattern is not a valid regular expression
if e != nil { if e != nil {
errorExit("invalid regular expression: " + e.Error()) return Delimiter{str: &str}
} }
return rx
// 3. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
} }
func isAlphabet(char uint8) bool { func isAlphabet(char uint8) bool {
@@ -513,7 +518,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
case "delete-char": case "delete-char":
keymap[key] = actDeleteChar keymap[key] = actDeleteChar
case "delete-char/eof": case "delete-char/eof":
keymap[key] = actDeleteCharEof keymap[key] = actDeleteCharEOF
case "end-of-line": case "end-of-line":
keymap[key] = actEndOfLine keymap[key] = actEndOfLine
case "cancel": case "cancel":

View File

@@ -8,11 +8,59 @@ import (
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*") // Valid regex
tokens := rx.FindAllString("-*--*---**---", -1) delim := delimiterRegexp(".")
if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || if delim.regex == nil || delim.str != nil {
tokens[3] != "*" || tokens[4] != "---" { t.Error(delim)
t.Errorf("%s %s %d", rx, tokens, len(tokens)) }
// Broken regex -> string
delim = delimiterRegexp("[0-9")
if delim.regex != nil || *delim.str != "[0-9" {
t.Error(delim)
}
// Valid regex
delim = delimiterRegexp("[0-9]")
if delim.regex.String() != "[0-9]" || delim.str != nil {
t.Error(delim)
}
// Tab character
delim = delimiterRegexp("\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tab expression
delim = delimiterRegexp("\\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tabs -> regex
delim = delimiterRegexp("\t+")
if delim.regex == nil || delim.str != nil {
t.Error(delim)
}
}
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.regex != nil ||
string(tokens[0].text) != "-*" ||
string(tokens[1].text) != "--*" ||
string(tokens[2].text) != "---*" ||
string(tokens[3].text) != "*" ||
string(tokens[4].text) != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens))
}
}
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.str != nil ||
string(tokens[0].text) != "-*--*" ||
string(tokens[1].text) != "---*" ||
string(tokens[2].text) != "*---" {
t.Errorf("%s %d", tokens, len(tokens))
} }
} }

View File

@@ -39,12 +39,13 @@ type term struct {
type Pattern struct { type Pattern struct {
mode Mode mode Mode
caseSensitive bool caseSensitive bool
forward bool
text []rune text []rune
terms []term terms []term
hasInvTerm bool hasInvTerm bool
delimiter *regexp.Regexp delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, *[]rune, []rune) (int, int) procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
} }
var ( var (
@@ -70,8 +71,8 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(mode Mode, caseMode Case, forward bool,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
switch mode { switch mode {
@@ -109,12 +110,13 @@ func BuildPattern(mode Mode, caseMode Case,
ptr := &Pattern{ ptr := &Pattern{
mode: mode, mode: mode,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
forward: forward,
text: []rune(asString), text: []rune(asString),
terms: terms, terms: terms,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -151,6 +153,9 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
if mode == ModeExtended { if mode == ModeExtended {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else if mode == ModeExtendedExact {
typ = termFuzzy
text = text[1:]
} }
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
if strings.HasSuffix(text, "$") { if strings.HasSuffix(text, "$") {
@@ -285,7 +290,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
func (p *Pattern) fuzzyMatch(item *Item) (int, int) { func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -293,7 +298,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, term := range p.terms {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 { if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
if term.inv { if term.inv {
break break
} }
@@ -305,29 +310,27 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
return offsets return offsets
} }
func (p *Pattern) prepareInput(item *Item) *[]Token { func (p *Pattern) prepareInput(item *Item) []Token {
if item.transformed != nil { if item.transformed != nil {
return item.transformed return item.transformed
} }
var ret *[]Token var ret []Token
if len(p.nth) > 0 { if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
runes := []rune(*item.text) ret = []Token{Token{text: item.text, prefixLength: 0}}
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int),
tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) {
for _, part := range *tokens { for _, part := range tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength
} }
} }

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"reflect"
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
@@ -36,11 +37,11 @@ func TestParseTermsExtendedExact(t *testing.T) {
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 || terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 ||
terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 ||
terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 ||
terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 ||
terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 || terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 ||
terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 ||
terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
@@ -57,10 +58,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, pattern := BuildPattern(ModeExtended, CaseSmart, true,
[]Range{}, nil, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
runes := []rune("aabbcc abc") sidx, eidx := algo.ExactMatchNaive(
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text)
if sidx != 7 || eidx != 10 { if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
} }
@@ -69,11 +70,11 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$")) pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
runes := []rune(str) sidx, eidx := algo.EqualMatch(
sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected { if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
} }
@@ -85,17 +86,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc")) pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc")) pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc")) pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc")) pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc")) pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc")) pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -108,25 +109,23 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
strptr := func(str string) *string { pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
return &str tokens := Tokenize([]rune("junegunn"), Delimiter{})
}
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg"))
tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi")
for _, mode := range []Mode{ModeFuzzy, ModeExtended} { for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: strptr("junegunn"), text: []rune("junegunn"),
origText: strptr("junegunn.choi"), origText: &origRunes,
transformed: trans}, transformed: trans},
} }
pattern.mode = mode pattern.mode = mode
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans { !reflect.DeepEqual(matches[0].transformed, trans) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
} }

View File

@@ -11,7 +11,7 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func(string) bool pusher func([]byte) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
} }
@@ -37,13 +37,14 @@ func (r *Reader) feed(src io.Reader) {
} }
reader := bufio.NewReader(src) reader := bufio.NewReader(src)
for { for {
line, err := reader.ReadString(delim) // ReadBytes returns err != nil if and only if the returned data does not
if line != "" { // end in delim.
// "ReadString returns err != nil if and only if the returned data does not end in delim." bytea, err := reader.ReadBytes(delim)
if len(bytea) > 0 {
if err == nil { if err == nil {
line = line[:len(line)-1] bytea = bytea[:len(bytea)-1]
} }
if r.pusher(line) { if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
} }
} }

View File

@@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s string) bool { strs = append(strs, s); return true }, pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox

View File

@@ -42,6 +42,7 @@ type Terminal struct {
history *History history *History
cycle bool cycle bool
header []string header []string
ansi bool
margin [4]string margin [4]string
marginInt [4]int marginInt [4]int
count int count int
@@ -106,7 +107,7 @@ const (
actCancel actCancel
actClearScreen actClearScreen
actDeleteChar actDeleteChar
actDeleteCharEof actDeleteCharEOF
actEndOfLine actEndOfLine
actForwardChar actForwardChar
actForwardWord actForwardWord
@@ -141,7 +142,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlG] = actAbort keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteCharEof keymap[C.CtrlD] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar keymap[C.CtrlH] = actBackwardDeleteChar
@@ -207,6 +208,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
marginInt: [4]int{0, 0, 0, 0}, marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: opts.Header, header: opts.Header,
ansi: opts.Ansi,
reading: true, reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
@@ -288,7 +290,7 @@ func (t *Terminal) output() {
if len(t.selected) == 0 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString()) fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
} }
} else { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@@ -436,15 +438,15 @@ func (t *Terminal) printHeader() {
} }
line := idx + 2 line := idx + 2
if t.inlineInfo { if t.inlineInfo {
line -= 1 line--
} }
if line >= max { if line >= max {
continue continue
} }
trimmed, colors, newState := extractColor(&lineStr, state) trimmed, colors, newState := extractColor(lineStr, state)
state = newState state = newState
item := &Item{ item := &Item{
text: trimmed, text: []rune(trimmed),
index: 0, index: 0,
colors: colors, colors: colors,
rank: Rank{0, 0, 0}} rank: Rank{0, 0, 0}}
@@ -462,7 +464,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
line := i + 2 + len(t.header) line := i + 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
line -= 1 line--
} }
t.move(line, 0, true) t.move(line, 0, true)
if i < count { if i < count {
@@ -537,7 +539,8 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
// Overflow // Overflow
text := []rune(*item.text) text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
@@ -804,7 +807,7 @@ func (t *Terminal) Loop() {
} }
selectItem := func(item *Item) bool { selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found { if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()} t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
return true return true
} }
return false return false
@@ -842,7 +845,7 @@ func (t *Terminal) Loop() {
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString()) executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
@@ -862,7 +865,7 @@ func (t *Terminal) Loop() {
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
t.delChar() t.delChar()
case actDeleteCharEof: case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
@@ -1013,7 +1016,7 @@ func (t *Terminal) Loop() {
} }
min := 2 + len(t.header) min := 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
min -= 1 min--
} }
if me.Double { if me.Double {
// Double-click // Double-click
@@ -1099,7 +1102,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header) max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
max += 1 max++
} }
return util.Max(max, 0) return util.Max(max, 0)
} }

View File

@@ -18,10 +18,16 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length // Token contains the tokenized part of the strings and its prefix length
type Token struct { type Token struct {
text *[]rune text []rune
prefixLength int prefixLength int
} }
// Delimiter for tokenizing the input
type Delimiter struct {
regex *regexp.Regexp
str *string
}
func newRange(begin int, end int) Range { func newRange(begin int, end int) Range {
if begin == 1 { if begin == 1 {
begin = rangeEllipsis begin = rangeEllipsis
@@ -68,16 +74,15 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens []string, begin int) []Token { func withPrefixLengths(tokens [][]rune, begin int) []Token {
ret := make([]Token, len(tokens)) ret := make([]Token, len(tokens))
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take // Need to define a new local variable instead of the reused token to take
// the pointer to it // the pointer to it
runes := []rune(token) ret[idx] = Token{text: token, prefixLength: prefixLength}
ret[idx] = Token{text: &runes, prefixLength: prefixLength} prefixLength += len(token)
prefixLength += len([]rune(token))
} }
return ret return ret
} }
@@ -88,13 +93,13 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input *string) ([]string, int) { func awkTokenizer(input []rune) ([][]rune, int) {
// 9, 32 // 9, 32
ret := []string{} ret := [][]rune{}
str := []rune{} str := []rune{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
for _, r := range []rune(*input) { for _, r := range input {
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
@@ -113,47 +118,69 @@ func awkTokenizer(input *string) ([]string, int) {
if white { if white {
str = append(str, r) str = append(str, r)
} else { } else {
ret = append(ret, string(str)) ret = append(ret, str)
state = awkBlack state = awkBlack
str = []rune{r} str = []rune{r}
} }
} }
} }
if len(str) > 0 { if len(str) > 0 {
ret = append(ret, string(str)) ret = append(ret, str)
} }
return ret, prefixLength return ret, prefixLength
} }
// Tokenize tokenizes the given string with the delimiter // Tokenize tokenizes the given string with the delimiter
func Tokenize(str *string, delimiter *regexp.Regexp) []Token { func Tokenize(runes []rune, delimiter Delimiter) []Token {
if delimiter == nil { if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*) // AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(str) tokens, prefixLength := awkTokenizer(runes)
return withPrefixLengths(tokens, prefixLength) return withPrefixLengths(tokens, prefixLength)
} }
tokens := delimiter.FindAllString(*str, -1)
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens *[]Token) *string { var tokens []string
ret := "" if delimiter.str != nil {
for _, token := range *tokens { tokens = strings.Split(string(runes), *delimiter.str)
ret += string(*token.text) for i := 0; i < len(tokens)-1; i++ {
tokens[i] = tokens[i] + *delimiter.str
}
} else if delimiter.regex != nil {
str := string(runes)
for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str)
if loc == nil {
loc = []int{0, len(str)}
}
last := util.Max(loc[1], 1)
tokens = append(tokens, str[:last])
str = str[last:]
}
} }
return &ret asRunes := make([][]rune, len(tokens))
for i, token := range tokens {
asRunes[i] = []rune(token)
}
return withPrefixLengths(asRunes, 0)
} }
func joinTokensAsRunes(tokens *[]Token) *[]rune { func joinTokens(tokens []Token) []rune {
ret := []rune{} ret := []rune{}
for _, token := range *tokens { for _, token := range tokens {
ret = append(ret, *token.text...) ret = append(ret, token.text...)
} }
return &ret return ret
}
func joinTokensAsRunes(tokens []Token) []rune {
ret := []rune{}
for _, token := range tokens {
ret = append(ret, token.text...)
}
return ret
} }
// Transform is used to transform the input when --with-nth option is given // Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *[]Token { func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth)) transTokens := make([]Token, len(withNth))
numTokens := len(tokens) numTokens := len(tokens)
for idx, r := range withNth { for idx, r := range withNth {
@@ -162,14 +189,14 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
part = append(part, *joinTokensAsRunes(&tokens)...) part = append(part, joinTokensAsRunes(tokens)...)
} else { } else {
if idx < 0 { if idx < 0 {
idx += numTokens + 1 idx += numTokens + 1
} }
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
minIdx = idx - 1 minIdx = idx - 1
part = append(part, *tokens[idx-1].text...) part = append(part, tokens[idx-1].text...)
} }
} }
} else { } else {
@@ -196,7 +223,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
minIdx = util.Max(0, begin-1) minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ { for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
part = append(part, *tokens[idx-1].text...) part = append(part, tokens[idx-1].text...)
} }
} }
} }
@@ -206,7 +233,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{&part, prefixLength} transTokens[idx] = Token{part, prefixLength}
} }
return &transTokens return transTokens
} }

View File

@@ -43,14 +43,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) { func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: def: ghi "
tokens := Tokenize(&input, nil) tokens := Tokenize([]rune(input), Delimiter{})
if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize(&input, delimiterRegexp(":")) tokens = Tokenize([]rune(input), delimiterRegexp(":"))
if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
t.Errorf("%s", tokens)
}
// With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }
@@ -58,39 +67,39 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) { func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl" input := " abc: def: ghi: jkl"
{ {
tokens := Tokenize(&input, nil) tokens := Tokenize([]rune(input), Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: " { if string(joinTokens(tx)) != "abc: def: ghi: " {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(*tx) != 4 || len(tx) != 4 ||
string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 || string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 || string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 || string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 { string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
} }
{ {
tokens := Tokenize(&input, delimiterRegexp(":")) tokens := Tokenize([]rune(input), delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(*tx) != 4 || len(tx) != 4 ||
string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 || string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 || string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 || string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 { string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import "C"
import ( import (
"os" "os"
"time" "time"
"unicode/utf8"
) )
// Max returns the largest integer // Max returns the largest integer
@@ -19,7 +20,7 @@ func Max(first int, items ...int) int {
return max return max
} }
// Max32 returns the smallest 32-bit integer // Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 { func Min32(first int32, second int32) int32 {
if first <= second { if first <= second {
return first return first
@@ -69,22 +70,33 @@ func DurWithin(
return val return val
} }
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
func TrimRight(runes *[]rune) []rune { func TrimRight(runes []rune) []rune {
var i int var i int
for i = len(*runes) - 1; i >= 0; i-- { for i = len(runes) - 1; i >= 0; i-- {
char := (*runes)[i] char := runes[i]
if char != ' ' && char != '\t' { if char != ' ' && char != '\t' {
break break
} }
} }
return (*runes)[0 : i+1] return runes[0 : i+1]
}
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
return runes
} }

View File

@@ -501,6 +501,43 @@ class TestGoFZF < TestBase
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
end end
def test_tiebreak_length_with_nth
input = %w[
1:hell
123:hello
12345:he
1234567:h
]
writelines tempname, input
output = %w[
1:hell
12345:he
123:hello
1234567:h
]
assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
output = %w[
1234567:h
12345:he
1:hell
123:hello
]
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
end
def test_tiebreak_end_backward_scan
input = %w[
foobar-fb
fubar
]
writelines tempname, input
assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/)
end
def test_invalid_cache def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' } tmux.until { |lines| lines[-2].include? '2/3' }
@@ -743,6 +780,27 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_invalid_term
tmux.send_keys "TERM=xxx fzf", :Enter
tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } }
end
def test_with_nth
writelines tempname, ['hello world ', 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
end
def test_with_nth_ansi
writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp
end
def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines tempname, [src, 'byebye']
assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path