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

Compare commits

...

30 Commits

Author SHA1 Message Date
Junegunn Choi
b86838c2b0 0.13.5 2016-08-21 05:02:45 +09:00
Junegunn Choi
1f7d1f9b15 Update Centos Dockerfile to use Go 1.7 2016-08-21 04:54:53 +09:00
Junegunn Choi
f8fdf9618a No need to cache the result in filtering mode (--filter) 2016-08-20 02:06:57 +09:00
Junegunn Choi
827a83efbc Remove Offset slice from Result struct 2016-08-20 01:53:32 +09:00
Junegunn Choi
3e88849386 [vim] Fix "E706: Variable type mismatch for: arg" 2016-08-19 18:02:32 +09:00
Junegunn Choi
608c416207 Add missing sources 2016-08-19 03:27:42 +09:00
Junegunn Choi
62f6ff9d6c [vim] Make arguments to fzf#wrap() optional
fzf#wrap([name string,] [opts dict,] [fullscreen boolean])
2016-08-19 03:05:22 +09:00
Junegunn Choi
37dc273148 Micro-optimizations
- Make structs smaller
- Introduce Result struct and use it to represent matched items instead of
  reusing Item struct for that purpose
- Avoid unnecessary memory allocation
- Avoid growing slice from the initial capacity
- Code cleanup
2016-08-19 02:39:32 +09:00
Junegunn Choi
f7f01d109e Set the upper limit of the number of search go routines 2016-08-19 01:55:38 +09:00
Junegunn Choi
01ee335521 Remove duplicate code 2016-08-18 03:11:54 +09:00
Junegunn Choi
0e0de29b87 Inline function calls in tight loops
By only using leaf functions
2016-08-18 01:48:52 +09:00
Junegunn Choi
babf877fd6 Increase the number of go routines for search
Sort performance increases as the size of each sublist decreases (n in
nlog(n) decreases). Merger is then responsible for merging the sorted
lists in order, and since in most cases we are only interesed in the
matches in the first page on the screen so the overhead in the process
is negligible.
2016-08-18 01:46:05 +09:00
Junegunn Choi
935272824e Setting GOMAXPROCS is no longer needed
https://golang.org/doc/go1.5
2016-08-17 02:21:33 +09:00
Junegunn Choi
3a9532c8fd Increase read buffer size to 64KB 2016-08-16 02:06:15 +09:00
Junegunn Choi
c4c92142a6 0.13.4 2016-08-14 18:10:21 +09:00
Junegunn Choi
d4b6338102 Lint 2016-08-14 17:51:34 +09:00
Junegunn Choi
8df7d962e6 Improve rendering time of long lines 2016-08-14 17:44:11 +09:00
Junegunn Choi
41e916a511 [perf] evaluateBonus can start from sidx - 1 2016-08-14 11:58:47 +09:00
Junegunn Choi
d9c8a9a880 [perf] Remove memory copy when using string delimiter 2016-08-14 04:30:55 +09:00
Junegunn Choi
ddc7bb9064 [perf] Optimize AWK-style tokenizer for --nth
Approx. 50% less memory footprint and 40% improvement in query time
2016-08-14 02:19:29 +09:00
Junegunn Choi
1d4057c209 [perf] Avoid allocating rune array for ascii string
In the best case (all ascii), this reduces the memory footprint by 60%
and the response time by 15% to 20%. In the worst case (every line has
non-ascii characters), 3 to 4% overhead is observed.
2016-08-14 00:41:30 +09:00
Junegunn Choi
822b86942c [test] Clear environment variables 2016-08-13 19:26:36 +09:00
Junegunn Choi
1e74dbb937 :hidden property of previous --preview-window should be cleared
Fix #636. Patch suggested by @edi9999.
2016-08-12 01:16:59 +09:00
Junegunn Choi
7cef92fffe [vim] Delete fzf buffer even when exit status is non-zero
Fix #183
2016-08-02 03:30:17 +09:00
Junegunn Choi
42e4992f06 [vim] Make sure to delete fzf buffer
Close junegunn/fzf.vim#173 and #630
2016-08-02 02:25:02 +09:00
Junegunn Choi
a6066175c6 Merge pull request #630 from kassio/master
Remove `name` option from `termopen`.
2016-07-30 19:05:43 +09:00
Kassio Borges
27444d6b1e Remove name option from termopen.
`termopen` no longer accepts a `name` option, instead we should suffix the
command with `;#NAME`.
2016-07-29 11:10:46 +01:00
Junegunn Choi
d6a99c0391 [vim] v:shell_error can change around redraw!
Patch suggested by Mariusz Atamańczuk
2016-07-28 01:41:11 +09:00
Junegunn Choi
f787f7e651 [vim] Add fzf#wrap helper function
Close #627
2016-07-26 02:37:12 +09:00
Junegunn Choi
a7c9c08371 [vim] Make :FZF command configurable with g:fzf_layout
To make it consistent with the other commands in fzf.vim
2016-07-21 01:47:08 +09:00
38 changed files with 1343 additions and 903 deletions

View File

@@ -1,6 +1,19 @@
CHANGELOG
=========
0.13.5
------
- Memory and performance optimization
- Up to 2x performance with half the amount of memory
0.13.4
------
- Performance optimization
- Memory footprint for ascii string is reduced by 60%
- 15 to 20% improvement of query performance
- Up to 45% better performance of `--nth` with non-regex delimiters
- Fixed invalid handling of `hidden` property of `--preview-window`
0.13.3
------
- Fixed duplicate rendering of the last line in preview window

View File

@@ -320,10 +320,10 @@ customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])`
#### `fzf#run`
For more advanced uses, you can use `fzf#run()` function with the following
options.
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -342,6 +342,17 @@ options.
Examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### `fzf#wrap`
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
Tips
----

View File

@@ -2,8 +2,8 @@
set -u
[[ "$@" =~ --pre ]] && version=0.13.3 pre=1 ||
version=0.13.3 pre=0
[[ "$@" =~ --pre ]] && version=0.13.5 pre=1 ||
version=0.13.5 pre=0
auto_completion=
key_bindings=

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Jul 2016" "fzf 0.13.3" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Aug 2016" "fzf 0.13.5" "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 "Jul 2016" "fzf 0.13.3" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Aug 2016" "fzf 0.13.5" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder

View File

@@ -21,7 +21,8 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_height = '40%'
let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0
@@ -104,11 +105,114 @@ function! s:warn(msg)
echohl None
endfunction
function! s:has_any(dict, keys)
for key in a:keys
if has_key(a:dict, key)
return 1
endif
endfor
return 0
endfunction
function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p')
return
endif
execute a:cmd s:escape(a:target)
endfunction
function! s:common_sink(action, lines) abort
if len(a:lines) < 2
return
endif
let key = remove(a:lines, 0)
let cmd = get(a:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
call s:open(cmd, item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
let expects = map(copy(args), 'type(v:val)')
let tidx = 0
for arg in copy(a:000)
let tidx = index(expects, type(arg), tidx)
if tidx < 0
throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
endif
let args[tidx] = arg
let tidx += 1
unlet arg
endfor
let [name, opts, bang] = args
" Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang
for key in s:layout_keys
if has_key(opts, key)
call remove(opts, key)
endif
endfor
elseif !s:has_any(opts, s:layout_keys)
if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height
else
let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout))
endif
endif
" History: g:fzf_history_dir
let opts.options = get(opts, 'options', '')
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir)
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options])
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
endif
return opts
endfunction
function! fzf#run(...) abort
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('term://*:FZF')
if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"'))
call s:warn('FZF is already running!')
return []
endif
@@ -137,7 +241,7 @@ try
call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|'
else
throw 'Invalid source type'
throw 'invalid source type'
endif
else
let prefix = ''
@@ -252,8 +356,9 @@ function! s:execute(dict, command, temps) abort
let command = escaped
endif
execute 'silent !'.command
let exit_status = v:shell_error
redraw!
return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : []
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction
function! s:execute_tmux(dict, command, temps) abort
@@ -264,8 +369,9 @@ function! s:execute_tmux(dict, command, temps) abort
endif
call system(command)
let exit_status = v:shell_error
redraw!
return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : []
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction
function! s:calc_size(max, val, dict)
@@ -326,7 +432,7 @@ endfunction
function! s:execute_term(dict, command, temps) abort
let [ppos, winopts] = s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'name': 'FZF', 'winopts': winopts, 'command': a:command }
\ 'winopts': winopts, 'command': a:command }
function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode
@@ -354,6 +460,10 @@ function! s:execute_term(dict, command, temps) abort
execute self.ppos.win.'wincmd w'
endif
if bufexists(self.buf)
execute 'bd!' self.buf
endif
if !s:exit_handler(a:code, self.command, 1)
return
endif
@@ -368,7 +478,7 @@ function! s:execute_term(dict, command, temps) abort
if s:present(a:dict, 'dir')
execute 'lcd' s:escape(a:dict.dir)
endif
call termopen(a:command, fzf)
call termopen(a:command . ';#FZF', fzf)
finally
if s:present(a:dict, 'dir')
lcd -
@@ -435,59 +545,20 @@ function! s:callback(dict, lines) abort
endfunction
let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
execute cmd s:escape(item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:cmd(bang, ...) abort
let s:action = get(g:, 'fzf_action', s:default_action)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let args = copy(a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif
if !a:bang
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save

View File

@@ -11,18 +11,18 @@ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
# Install Go 1.5
# Install Go 1.7
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \
tar -xz && mv go go1.5
https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \
tar -xz && mv go go1.7
# Install RPMs for building static 32-bit binary
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.5
ENV PATH /go1.5/bin:$PATH
ENV GOROOT /go1.7
ENV PATH /go1.7/bin:$PATH
# For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash

View File

@@ -15,21 +15,21 @@ import (
* In short: They try to do as little work as possible.
*/
func runeAt(runes []rune, index int, max int, forward bool) rune {
func indexAt(index int, max int, forward bool) int {
if forward {
return runes[index]
return index
}
return runes[max-index-1]
return max - index - 1
}
// Result conatins the results of running a match function.
type Result struct {
Start int32
End int32
Start int
End int
// Items are basically sorted by the lengths of matched substrings.
// But we slightly adjust the score with bonus for better results.
Bonus int32
Bonus int
}
type charClass int
@@ -42,14 +42,14 @@ const (
charNumber
)
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 {
var bonus int32
func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int {
var bonus int
pidx := 0
lenPattern := len(pattern)
consecutive := false
prevClass := charNonWord
for index := 0; index < eidx; index++ {
char := runes[index]
for index := util.Max(0, sidx-1); index < eidx; index++ {
char := text.Get(index)
var class charClass
if unicode.IsLower(char) {
class = charLower
@@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
class = charNonWord
}
var point int32
var point int
if prevClass == charNonWord && class != charNonWord {
// Word boundary
point = 2
@@ -107,7 +107,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
}
// FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 {
return Result{0, 0, 0}
}
@@ -125,11 +125,11 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
sidx := -1
eidx := -1
lenRunes := len(runes)
lenRunes := text.Length()
lenPattern := len(pattern)
for index := range runes {
char := runeAt(runes, index, lenRunes, forward)
for index := 0; index < lenRunes; index++ {
char := text.Get(indexAt(index, lenRunes, forward))
// This is considerably faster than blindly applying strings.ToLower to the
// whole string
if !caseSensitive {
@@ -142,7 +142,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := runeAt(pattern, pidx, lenPattern, forward)
pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar {
if sidx < 0 {
sidx = index
@@ -157,7 +157,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
if sidx >= 0 && eidx >= 0 {
pidx--
for index := eidx - 1; index >= sidx; index-- {
char := runeAt(runes, index, lenRunes, forward)
char := text.Get(indexAt(index, lenRunes, forward))
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -166,7 +166,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
}
}
pchar := runeAt(pattern, pidx, lenPattern, forward)
pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar {
if pidx--; pidx < 0 {
sidx = index
@@ -181,8 +181,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
sidx, eidx = lenRunes-eidx, lenRunes-sidx
}
return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
}
return Result{-1, -1, 0}
}
@@ -194,12 +194,12 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
//
// We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 {
return Result{0, 0, 0}
}
lenRunes := len(runes)
lenRunes := text.Length()
lenPattern := len(pattern)
if lenRunes < lenPattern {
@@ -208,7 +208,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
pidx := 0
for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward)
char := text.Get(indexAt(index, lenRunes, forward))
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -216,7 +216,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := runeAt(pattern, pidx, lenPattern, forward)
pchar := pattern[indexAt(pidx, lenPattern, forward)]
if pchar == char {
pidx++
if pidx == lenPattern {
@@ -228,8 +228,8 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
sidx = lenRunes - (index + 1)
eidx = lenRunes - (index - lenPattern + 1)
}
return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
}
} else {
index -= pidx
@@ -240,13 +240,13 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
}
// PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
if len(runes) < len(pattern) {
func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if text.Length() < len(pattern) {
return Result{-1, -1, 0}
}
for index, r := range pattern {
char := runes[index]
char := text.Get(index)
if !caseSensitive {
char = unicode.ToLower(char)
}
@@ -255,21 +255,20 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
}
}
lenPattern := len(pattern)
return Result{0, int32(lenPattern),
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)}
return Result{0, lenPattern,
evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)}
}
// SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result {
runes := util.TrimRight(input)
trimmedLen := len(runes)
func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
trimmedLen := text.Length() - text.TrailingWhitespaces()
diff := trimmedLen - len(pattern)
if diff < 0 {
return Result{-1, -1, 0}
}
for index, r := range pattern {
char := runes[index+diff]
char := text.Get(index + diff)
if !caseSensitive {
char = unicode.ToLower(char)
}
@@ -280,22 +279,22 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune)
lenPattern := len(pattern)
sidx := trimmedLen - lenPattern
eidx := trimmedLen
return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
}
// EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
// Note: EqualMatch always return a zero bonus.
if len(runes) != len(pattern) {
if text.Length() != len(pattern) {
return Result{-1, -1, 0}
}
runesStr := string(runes)
runesStr := text.ToString()
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
if runesStr == string(pattern) {
return Result{0, int32(len(pattern)), 0}
return Result{0, len(pattern), 0}
}
return Result{-1, -1, 0}
}

View File

@@ -3,13 +3,15 @@ package algo
import (
"strings"
"testing"
"github.com/junegunn/fzf/src/util"
)
func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) {
func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, bonus int) {
if !caseSensitive {
pattern = strings.ToLower(pattern)
}
res := fun(caseSensitive, forward, []rune(input), []rune(pattern))
res := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern))
if res.Start != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern)
}

View File

@@ -36,7 +36,7 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) {
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset
var output bytes.Buffer
@@ -84,7 +84,10 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if proc != nil {
proc(rest, state)
}
return output.String(), offsets, state
if len(offsets) == 0 {
return output.String(), nil, state
}
return output.String(), &offsets, state
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -16,7 +16,7 @@ func TestExtractColor(t *testing.T) {
src := "hello world"
var state *ansiState
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, nil)
state = newState
if output != "hello world" {
@@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) {
assertion(ansiOffsets, state)
}
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if offsets != nil {
t.Fail()
}
})
state = nil
src = "\x1b[0mhello world"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if offsets != nil {
t.Fail()
}
})
state = nil
src = "\x1b[1mhello world"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
assert((*offsets)[0], 0, 11, -1, -1, true)
})
state = nil
src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 6, -1, -1, true)
assert((*offsets)[0], 0, 6, -1, -1, true)
})
state = nil
src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
assert((*offsets)[0], 0, 11, -1, -1, true)
})
state = nil
src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
assert((*offsets)[0], 6, 11, 4, 5, true)
})
state = nil
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
assert((*offsets)[0], 6, 11, 4, 5, true)
})
state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 9, 4, 5, true)
assert((*offsets)[0], 6, 9, 4, 5, true)
})
state = nil
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 3 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 3 {
t.Fail()
}
assert(offsets[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false)
assert((*offsets)[0], 6, 8, 4, 233, true)
assert((*offsets)[1], 8, 9, 161, 233, true)
assert((*offsets)[2], 10, 11, 161, -1, false)
})
// {38,48};5;{38,48}
state = nil
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 2 {
t.Fail()
}
assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true)
assert((*offsets)[0], 6, 9, 38, 48, true)
assert((*offsets)[1], 9, 10, 48, 38, true)
})
src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 6, 11, 2, -1, true)
assert((*offsets)[0], 6, 11, 2, -1, true)
})
src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 0, 11, 2, -1, true)
assert((*offsets)[0], 0, 11, 2, -1, true)
})
src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 {
check(func(offsets *[]ansiOffset, state *ansiState) {
if len(*offsets) != 2 {
t.Fail()
}
if state.fg != 200 || state.bg != 100 || state.bold {
t.Fail()
}
assert(offsets[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false)
assert((*offsets)[0], 0, 6, 2, -1, true)
assert((*offsets)[1], 6, 11, 200, 100, false)
})
}

View File

@@ -3,7 +3,7 @@ package fzf
import "sync"
// queryCache associates strings to lists of items
type queryCache map[string][]*Item
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 []*Item) {
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return
}
@@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
}
// Find is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
if len(key) == 0 || !chunk.IsFull() {
return nil, false
}

View File

@@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) {
chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{}
chunk2p := &chunk2
items1 := []*Item{&Item{}}
items2 := []*Item{&Item{}, &Item{}}
items1 := []*Result{&Result{}}
items2 := []*Result{&Result{}, &Result{}}
cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2)

View File

@@ -3,6 +3,8 @@ package fzf
import (
"fmt"
"testing"
"github.com/junegunn/fzf/src/util"
)
func TestChunkList(t *testing.T) {
@@ -10,7 +12,7 @@ func TestChunkList(t *testing.T) {
sortCriteria = []criterion{byMatchLen, byLength}
cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))}
return &Item{text: util.ToChars(s), index: int32(i * 2)}
})
// Snapshot
@@ -39,11 +41,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items")
}
last := func(arr [5]int32) int32 {
return arr[len(arr)-1]
}
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 ||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 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

@@ -8,7 +8,7 @@ import (
const (
// Current version
version = "0.13.3"
version = "0.13.5"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -16,6 +16,7 @@ const (
// Reader
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
readerBufferSize = 64 * 1024
// Terminal
initialDelay = 20 * time.Millisecond

View File

@@ -28,16 +28,11 @@ package fzf
import (
"fmt"
"os"
"runtime"
"time"
"github.com/junegunn/fzf/src/util"
)
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
/*
Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart)
@@ -49,8 +44,6 @@ Matcher -> EvtHeader -> Terminal (update header)
// Run starts fzf
func Run(opts *Options) {
initProcs()
sort := opts.Sort > 0
sortCriteria = opts.Criteria
@@ -63,29 +56,29 @@ func Run(opts *Options) {
eventBox := util.NewEventBox()
// ANSI code processor
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
return util.BytesToRunes(data), nil
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return 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) ([]rune, []ansiOffset) {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState
return []rune(trimmed), offsets
return util.RunesToChars([]rune(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) ([]rune, []ansiOffset) {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil)
return []rune(trimmed), nil
return util.RunesToChars([]rune(trimmed)), nil
}
}
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
}
@@ -100,29 +93,28 @@ func Run(opts *Options) {
eventBox.Set(EvtHeader, header)
return nil
}
runes, colors := ansiProcessor(data)
chars, colors := ansiProcessor(data)
return &Item{
text: runes,
colors: colors,
rank: buildEmptyRank(int32(index))}
index: int32(index),
text: chars,
colors: colors}
})
} else {
chunkList = NewChunkList(func(data []byte, index int) *Item {
runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header)
return nil
}
textRunes := joinTokens(trans)
item := Item{
text: joinTokens(trans),
origText: &runes,
colors: nil,
rank: buildEmptyRank(int32(index))}
index: int32(index),
origText: &data,
colors: nil}
trimmed, colors := ansiProcessorRunes(item.text)
trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed
item.colors = colors
return &item
@@ -151,7 +143,7 @@ func Run(opts *Options) {
}
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Fuzzy, opts.Extended, opts.Case, forward,
opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Filter == nil,
opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -169,10 +161,12 @@ func Run(opts *Options) {
reader := Reader{
func(runes []byte) bool {
item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) {
fmt.Println(string(item.text))
if item != nil {
if result, _ := pattern.MatchItem(item); result != nil {
fmt.Println(item.text.ToString())
found = true
}
}
return false
}, eventBox, opts.ReadZero}
reader.ReadSource()
@@ -185,7 +179,7 @@ func Run(opts *Options) {
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString(opts.Ansi))
fmt.Println(merger.Get(i).item.AsString(opts.Ansi))
found = true
}
}
@@ -265,7 +259,7 @@ func Run(opts *Options) {
fmt.Println()
}
for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString(opts.Ansi))
fmt.Println(val.Get(i).item.AsString(opts.Ansi))
}
if count > 0 {
os.Exit(exitOk)

View File

@@ -1,291 +1,39 @@
package fzf
import (
"math"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
// Offset holds three 32-bit integers denoting the offsets of a matched substring
type Offset [3]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line
type Item struct {
text []rune
origText *[]rune
index int32
text util.Chars
origText *[]byte
colors *[]ansiOffset
transformed []Token
offsets []Offset
colors []ansiOffset
rank [5]int32
bonus int32
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
func isRankValid(rank [5]int32) bool {
// Exclude ordinal index
for _, r := range rank[:4] {
if r > 0 {
return true
}
}
return false
}
func buildEmptyRank(index int32) [5]int32 {
return [5]int32{0, 0, 0, 0, index}
}
// Index returns ordinal index of the Item
func (item *Item) Index() int32 {
return item.rank[4]
return item.index
}
// Rank calculates rank of the Item
func (item *Item) Rank(cache bool) [5]int32 {
if cache && isRankValid(item.rank) {
return item.rank
// Colors returns ansiOffsets of the Item
func (item *Item) Colors() []ansiOffset {
if item.colors == nil {
return []ansiOffset{}
}
matchlen := 0
prevEnd := 0
lenSum := 0
minBegin := math.MaxInt32
for _, offset := range item.offsets {
begin := int(offset[0])
end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := buildEmptyRank(item.Index())
for idx, criterion := range sortCriteria {
var val int32
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxInt32
} else {
// It is extremely unlikely that bonus exceeds 128
val = 128*int32(matchlen) - item.bonus
}
case byLength:
// It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
// If offsets is empty, lenSum will be 0, but we don't care
val = int32(lenSum)
} else {
val = int32(len(item.text))
}
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
for idx, r := range item.text {
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = int32(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = int32(1 + len(item.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
rank[idx] = val
}
if cache {
item.rank = rank
}
return rank
return *item.colors
}
// AsString returns the original string
func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr(stripAnsi)
}
// StringPtr returns the pointer to the original string
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil {
if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
return &trimmed
return trimmed
}
orig := string(*item.origText)
return &orig
return string(*item.origText)
}
str := string(item.text)
return &str
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Item
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank[idx]
right := jrank[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank[4] <= jrank[4]) != tac
return item.text.ToString()
}

View File

@@ -1,108 +1,23 @@
package fzf
import (
"math"
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
Offset{1, 3}, Offset{2, 9}}
sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
offsets[3][0] != 3 || offsets[3][1] != 5 {
t.Error("Invalid order:", offsets)
func TestStringPtr(t *testing.T) {
orig := []byte("\x1b[34mfoo")
text := []byte("\x1b[34mbar")
item := Item{origText: &orig, text: util.ToChars(text)}
if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
t.Fail()
}
if item.AsString(true) != "foo" {
t.Fail()
}
item.origText = nil
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
t.Fail()
}
}
func TestRankComparison(t *testing.T) {
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestItemRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}}
rank1 := item1.Rank(true)
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
item2 := Item{text: strs[0], offsets: []Offset{}}
items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 {
t.Error(items)
}
items = []*Item{&item2, &item1, &item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item2 ||
items[2] != &item1 || items[3] != &item1 {
t.Error(items)
}
// Sort by relevance
item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items))
if items[0] != &item6 || items[1] != &item4 ||
items[2] != &item5 || items[3] != &item3 ||
items[4] != &item2 || items[5] != &item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -43,7 +43,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
tac: tac,
eventBox: eventBox,
reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(),
partitions: util.Min(8*runtime.NumCPU(), 32),
mergerCache: make(map[string]*Merger)}
}
@@ -106,18 +106,19 @@ func (m *Matcher) Loop() {
}
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
perSlice := len(chunks) / m.partitions
partitions := m.partitions
perSlice := len(chunks) / partitions
// No need to parallelize
if perSlice == 0 {
return [][]*Chunk{chunks}
partitions = len(chunks)
perSlice = 1
}
slices := make([][]*Chunk, m.partitions)
for i := 0; i < m.partitions; i++ {
slices := make([][]*Chunk, partitions)
for i := 0; i < partitions; i++ {
start := i * perSlice
end := start + perSlice
if i == m.partitions-1 {
if i == partitions-1 {
end = len(chunks)
}
slices[i] = chunks[start:end]
@@ -127,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct {
index int
matches []*Item
matches []*Result
}
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@@ -154,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
waitGroup.Add(1)
go func(idx int, chunks []*Chunk) {
defer func() { waitGroup.Done() }()
sliceMatches := []*Item{}
for _, chunk := range chunks {
count := 0
allMatches := make([][]*Result, len(chunks))
for idx, chunk := range chunks {
matches := request.pattern.Match(chunk)
sliceMatches = append(sliceMatches, matches...)
allMatches[idx] = matches
count += len(matches)
if cancelled.Get() {
return
}
countChan <- len(matches)
}
sliceMatches := make([]*Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
@@ -199,12 +206,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
}
partialResults := make([][]*Item, numSlices)
for _, _ = range slices {
partialResults := make([][]*Result, numSlices)
for _ = range slices {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
return NewMerger(partialResults, m.sort, m.tac), false
return NewMerger(pattern, partialResults, m.sort, m.tac), false
}
// Reset is called to interrupt/signal the ongoing search

View File

@@ -2,14 +2,15 @@ package fzf
import "fmt"
// Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false, false)
// EmptyMerger is a Merger with no data
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 {
lists [][]*Item
merged []*Item
pattern *Pattern
lists [][]*Result
merged []*Result
chunks *[]*Chunk
cursors []int
sorted bool
@@ -22,6 +23,7 @@ type Merger struct {
// original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
mg := Merger{
pattern: nil,
chunks: chunks,
tac: tac,
count: 0}
@@ -33,10 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
}
// NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
mg := Merger{
pattern: pattern,
lists: lists,
merged: []*Item{},
merged: []*Result{},
chunks: nil,
cursors: make([]int, len(lists)),
sorted: sorted,
@@ -55,14 +58,14 @@ func (mg *Merger) Length() int {
return mg.count
}
// Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item {
// Get returns the pointer to the Result object indexed by the given integer
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 (*chunk)[idx%chunkSize]
return &Result{item: (*chunk)[idx%chunkSize]}
}
if mg.sorted {
@@ -86,9 +89,9 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax
}
func (mg *Merger) mergedGet(idx int) *Item {
func (mg *Merger) mergedGet(idx int) *Result {
for i := len(mg.merged); i <= idx; i++ {
minRank := buildEmptyRank(0)
minRank := minRank()
minIdx := -1
for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx]
@@ -97,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
continue
}
if cursor >= 0 {
rank := list[cursor].Rank(false)
rank := list[cursor].rank
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank
minIdx = listIdx

View File

@@ -5,6 +5,8 @@ import (
"math/rand"
"sort"
"testing"
"github.com/junegunn/fzf/src/util"
)
func assert(t *testing.T, cond bool, msg ...string) {
@@ -13,18 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
}
}
func randItem() *Item {
func randResult() *Result {
str := fmt.Sprintf("%d", rand.Uint32())
offsets := make([]Offset, rand.Int()%3)
for idx := range offsets {
sidx := int32(rand.Uint32() % 20)
eidx := sidx + int32(rand.Uint32()%20)
offsets[idx] = Offset{sidx, eidx}
}
return &Item{
text: []rune(str),
rank: buildEmptyRank(rand.Int31()),
offsets: offsets}
return &Result{
item: &Item{text: util.RunesToChars([]rune(str))},
rank: rank{index: rand.Int31()}}
}
func TestEmptyMerger(t *testing.T) {
@@ -34,23 +29,23 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
}
func buildLists(partiallySorted bool) ([][]*Item, []*Item) {
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
numLists := 4
lists := make([][]*Item, numLists)
lists := make([][]*Result, numLists)
cnt := 0
for i := 0; i < numLists; i++ {
numItems := rand.Int() % 20
cnt += numItems
lists[i] = make([]*Item, numItems)
for j := 0; j < numItems; j++ {
item := randItem()
numResults := rand.Int() % 20
cnt += numResults
lists[i] = make([]*Result, numResults)
for j := 0; j < numResults; j++ {
item := randResult()
lists[i][j] = item
}
if partiallySorted {
sort.Sort(ByRelevance(lists[i]))
}
}
items := []*Item{}
items := []*Result{}
for _, list := range lists {
items = append(items, list...)
}
@@ -62,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items)
// Not sorted: same order
mg := NewMerger(lists, false, false)
mg := NewMerger(nil, lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -74,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items)
// Sorted sorted order
mg := NewMerger(lists, true, false)
mg := NewMerger(nil, lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ {
@@ -84,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
}
// Inverse order
mg2 := NewMerger(lists, true, false)
mg2 := NewMerger(nil, lists, true, false)
for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -95,6 +95,7 @@ type criterion int
const (
byMatchLen criterion = iota
byBonus
byLength
byBegin
byEnd
@@ -178,7 +179,7 @@ func defaultOptions() *Options {
Delimiter: Delimiter{},
Sort: 1000,
Tac: false,
Criteria: []criterion{byMatchLen, byLength},
Criteria: []criterion{byMatchLen, byBonus, byLength},
Multi: false,
Ansi: false,
Mouse: true,
@@ -406,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string {
}
func parseTiebreak(str string) []criterion {
criteria := []criterion{byMatchLen}
criteria := []criterion{byMatchLen, byBonus}
hasIndex := false
hasLength := false
hasBegin := false
@@ -724,6 +725,7 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
func parsePreviewWindow(opts *previewOpts, input string) {
layout := input
opts.hidden = false
if strings.HasSuffix(layout, ":hidden") {
opts.hidden = true
layout = strings.TrimSuffix(layout, ":hidden")

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
func TestDelimiterRegex(t *testing.T) {
@@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
tokens := Tokenize(util.RunesToChars([]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) != "---" {
tokens[0].text.ToString() != "-*" ||
tokens[1].text.ToString() != "--*" ||
tokens[2].text.ToString() != "---*" ||
tokens[3].text.ToString() != "*" ||
tokens[4].text.ToString() != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens))
}
}
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
if delim.str != nil ||
string(tokens[0].text) != "-*--*" ||
string(tokens[1].text) != "---*" ||
string(tokens[2].text) != "*---" {
tokens[0].text.ToString() != "-*--*" ||
tokens[1].text.ToString() != "---*" ||
tokens[2].text.ToString() != "*---" {
t.Errorf("%s %d", tokens, len(tokens))
}
}
@@ -352,14 +353,14 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
}
func TestToggle(t *testing.T) {
optsFor := func(words ...string) *Options {
func optsFor(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
}
func TestToggle(t *testing.T) {
opts := optsFor()
if opts.ToggleSort {
t.Error()
@@ -375,3 +376,31 @@ func TestToggle(t *testing.T) {
t.Error()
}
}
func TestPreviewOpts(t *testing.T) {
opts := optsFor()
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posRight &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error()
}
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden")
if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true &&
opts.Preview.position == posLeft &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posDown &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error(opts.Preview)
}
}

View File

@@ -2,7 +2,6 @@ package fzf
import (
"regexp"
"sort"
"strings"
"github.com/junegunn/fzf/src/algo"
@@ -49,7 +48,7 @@ type Pattern struct {
cacheable bool
delimiter Delimiter
nth []Range
procFun map[termType]func(bool, bool, []rune, []rune) algo.Result
procFun map[termType]func(bool, bool, util.Chars, []rune) algo.Result
}
var (
@@ -76,7 +75,7 @@ func clearChunkCache() {
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern {
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string
if extended {
@@ -90,7 +89,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return cached
}
caseSensitive, cacheable := true, true
caseSensitive := true
termSets := []termSet{}
if extended {
@@ -100,7 +99,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
for idx, term := range termSet {
// If the query contains inverse search terms or OR operators,
// we cannot cache the search scope
if idx > 0 || term.inv {
if !cacheable || idx > 0 || term.inv {
cacheable = false
break Loop
}
@@ -125,7 +124,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
cacheable: cacheable,
nth: nth,
delimiter: delimiter,
procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)}
procFun: make(map[termType]func(bool, bool, util.Chars, []rune) algo.Result)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch
@@ -235,9 +234,7 @@ func (p *Pattern) CacheKey() string {
}
// Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk) []*Item {
space := chunk
func (p *Pattern) Match(chunk *Chunk) []*Result {
// ChunkCache: Exact match
cacheKey := p.CacheKey()
if p.cacheable {
@@ -246,7 +243,8 @@ func (p *Pattern) Match(chunk *Chunk) []*Item {
}
}
// ChunkCache: Prefix/suffix match
// Prefix/suffix cache
var space []*Result
Loop:
for idx := 1; idx < len(cacheKey); idx++ {
// [---------| ] | [ |---------]
@@ -256,14 +254,13 @@ Loop:
suffix := cacheKey[idx:]
for _, substr := range [2]*string{&prefix, &suffix} {
if cached, found := _cache.Find(chunk, *substr); found {
cachedChunk := Chunk(cached)
space = &cachedChunk
space = cached
break Loop
}
}
}
matches := p.matchChunk(space)
matches := p.matchChunk(chunk, space)
if p.cacheable {
_cache.Add(chunk, cacheKey, matches)
@@ -271,20 +268,19 @@ Loop:
return matches
}
func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{}
if !p.extended {
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result {
matches := []*Result{}
if space == nil {
for _, item := range *chunk {
offset, bonus := p.basicMatch(item)
if sidx := offset[0]; sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{offset}, bonus))
if match, _ := p.MatchItem(item); match != nil {
matches = append(matches, match)
}
}
} else {
for _, item := range *chunk {
if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) {
matches = append(matches, dupItem(item, offsets, bonus))
for _, result := range space {
if match, _ := p.MatchItem(result.item); match != nil {
matches = append(matches, match)
}
}
}
@@ -292,29 +288,22 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
}
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool {
if !p.extended {
offset, _ := p.basicMatch(item)
sidx := offset[0]
return sidx >= 0
func (p *Pattern) MatchItem(item *Item) (*Result, []Offset) {
if p.extended {
if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) {
return buildResult(item, offsets, bonus, trimLen), offsets
}
offsets, _ := p.extendedMatch(item)
return len(offsets) == len(p.termSets)
return nil, nil
}
offset, bonus, trimLen := p.basicMatch(item)
if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
return buildResult(item, offsets, bonus, trimLen), offsets
}
return nil, nil
}
func dupItem(item *Item, offsets []Offset, bonus int32) *Item {
sort.Sort(ByOrder(offsets))
return &Item{
text: item.text,
origText: item.origText,
transformed: item.transformed,
offsets: offsets,
bonus: bonus,
colors: item.colors,
rank: buildEmptyRank(item.Index())}
}
func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
func (p *Pattern) basicMatch(item *Item) (Offset, int, int) {
input := p.prepareInput(item)
if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
@@ -322,33 +311,39 @@ func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
}
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) {
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) {
input := p.prepareInput(item)
offsets := []Offset{}
var totalBonus int32
var totalBonus int
var totalTrimLen int
for _, termSet := range p.termSets {
var offset *Offset
var bonus int32
var offset Offset
var bonus int
var trimLen int
matched := false
for _, term := range termSet {
pfun := p.procFun[term.typ]
off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
if sidx := off[0]; sidx >= 0 {
if term.inv {
continue
}
offset, bonus = &off, pen
offset, bonus, trimLen = off, pen, tLen
matched = true
break
} else if term.inv {
offset, bonus = &Offset{0, 0, 0}, 0
offset, bonus, trimLen = Offset{0, 0}, 0, 0
matched = true
continue
}
}
if offset != nil {
offsets = append(offsets, *offset)
if matched {
offsets = append(offsets, offset)
totalBonus += bonus
totalTrimLen += trimLen
}
}
return offsets, totalBonus
return offsets, totalBonus, totalTrimLen
}
func (p *Pattern) prepareInput(item *Item) []Token {
@@ -357,26 +352,24 @@ func (p *Pattern) prepareInput(item *Item) []Token {
}
var ret []Token
if len(p.nth) > 0 {
if len(p.nth) == 0 {
ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}}
} else {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
} else {
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
}
item.transformed = ret
return ret
}
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) {
for _, part := range tokens {
prefixLength := int32(part.prefixLength)
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 {
var sidx int32 = res.Start + prefixLength
var eidx int32 = res.End + prefixLength
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 {
sidx := int32(res.Start) + part.prefixLength
eidx := int32(res.End) + part.prefixLength
return Offset{sidx, eidx}, res.Bonus, int(part.trimLength)
}
}
// TODO: math.MaxUint16
return Offset{-1, -1, -1}, 0.0
return Offset{-1, -1}, 0, -1
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
)
func TestParseTermsExtended(t *testing.T) {
@@ -68,10 +69,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true,
pattern := BuildPattern(true, true, CaseSmart, true, true,
[]Range{}, Delimiter{}, []rune("'abc"))
res := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text)
pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text)
if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
}
@@ -80,11 +81,11 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int32, eidxExpected int32) {
match := func(str string, sidxExpected int, eidxExpected int) {
res := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text)
pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text)
if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
}
@@ -96,17 +97,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
pat1 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
pat2 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
pat3 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
pat4 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
pat5 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
pat6 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -119,31 +120,37 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{})
pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi")
origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} {
chunk := Chunk{
&Item{
text: []rune("junegunn"),
origText: &origRunes,
text: util.RunesToChars([]rune("junegunn")),
origText: &origBytes,
transformed: trans},
}
pattern.extended = extended
matches := pattern.matchChunk(&chunk)
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
!reflect.DeepEqual(matches[0].transformed, trans) {
matches := pattern.matchChunk(&chunk, nil) // No cache
if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" ||
!reflect.DeepEqual(matches[0].item.transformed, trans) {
t.Error("Invalid match result", matches)
}
match, offsets := pattern.MatchItem(chunk[0])
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) {
t.Error("Invalid match result", match)
}
}
}
func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr))
pat := BuildPattern(true, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}

View File

@@ -34,7 +34,7 @@ func (r *Reader) feed(src io.Reader) {
if r.delimNil {
delim = '\000'
}
reader := bufio.NewReader(src)
reader := bufio.NewReaderSize(src, readerBufferSize)
for {
// ReadBytes returns err != nil if and only if the returned data does not
// end in delim.

258
src/result.go Normal file
View File

@@ -0,0 +1,258 @@
package fzf
import (
"math"
"sort"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
index int32
}
type rank struct {
// byMatchLen, byBonus, ...
points [5]uint16
index int32
}
type Result struct {
item *Item
rank rank
}
func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item, rank: rank{index: item.index}}
matchlen := 0
prevEnd := 0
minBegin := math.MaxInt32
numChars := item.text.Length()
for _, offset := range offsets {
begin := int(offset[0])
end := int(offset[1])
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
for idx, criterion := range sortCriteria {
var val uint16
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxUint16
} else {
val = util.AsUint16(matchlen)
}
case byBonus:
// Higher is better
val = math.MaxUint16 - util.AsUint16(bonus)
case byLength:
// If offsets is empty, trimLen will be 0, but we don't care
val = util.AsUint16(trimLen)
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = util.AsUint16(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = util.AsUint16(1 + numChars - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
result.rank.points[idx] = val
}
return &result
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
// Index returns ordinal index of the Item
func (result *Result) Index() int32 {
return result.item.index
}
func minRank() rank {
return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset {
itemColors := result.item.Colors()
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range matchOffsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range itemColors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var colors []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return colors
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Result
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Result
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
}
func compareRanks(irank rank, jrank rank, tac bool) bool {
for idx := 0; idx < 5; idx++ {
left := irank.points[idx]
right := jrank.points[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank.index <= jrank.index) != tac
}

115
src/result_test.go Normal file
View File

@@ -0,0 +1,115 @@
package fzf
import (
"math"
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
)
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
Offset{1, 3}, Offset{2, 9}}
sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
offsets[3][0] != 3 || offsets[3][1] != 5 {
t.Error("Invalid order:", offsets)
}
}
func TestRankComparison(t *testing.T) {
rank := func(vals ...uint16) rank {
return rank{
points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]},
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) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order")
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestResultRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byBonus, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3)
if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 {
t.Error(item1.rank)
}
// Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3)
items := []*Result{item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item1 {
t.Error(items)
}
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)
}
// Sort by relevance
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
items = []*Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if items[0] != item6 || items[1] != item4 ||
items[2] != item5 || items[3] != item3 ||
items[4] != item2 || items[5] != item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
item := Result{
item: &Item{
colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
colors := item.colorOffsets(offsets, 99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -77,7 +77,7 @@ type Terminal struct {
type selectedItem struct {
at time.Time
text *string
text string
}
type byTimeOrder []selectedItem
@@ -357,7 +357,7 @@ func (t *Terminal) output() bool {
}
} else {
for _, sel := range t.sortSelected() {
fmt.Println(*sel.text)
fmt.Println(sel.text)
}
}
return found
@@ -564,12 +564,11 @@ func (t *Terminal) printHeader() {
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState
item := &Item{
text: []rune(trimmed),
colors: colors,
rank: buildEmptyRank(0)}
text: util.RunesToChars([]rune(trimmed)),
colors: colors}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false)
}
}
@@ -590,7 +589,8 @@ func (t *Terminal) printList() {
}
}
func (t *Terminal) printItem(item *Item, i int, current bool) {
func (t *Terminal) printItem(result *Result, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := " "
if t.jumping != jumpDisabled {
@@ -609,14 +609,14 @@ func (t *Terminal) printItem(item *Item, i int, current bool) {
} else {
t.window.CPrint(C.ColCurrent, true, " ")
}
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true)
} else {
if selected {
t.window.CPrint(C.ColSelected, true, ">")
} else {
t.window.Print(" ")
}
t.printHighlighted(item, false, 0, C.ColMatch, false)
t.printHighlighted(result, false, 0, C.ColMatch, false)
}
}
@@ -656,29 +656,43 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed
}
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int
for _, offset := range item.offsets {
maxe = util.Max(maxe, int(offset[1]))
func overflow(runes []rune, max int) bool {
l := 0
for _, r := range runes {
l += runeWidth(r, l)
if l > max {
return true
}
}
return false
}
func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) {
item := result.item
// Overflow
text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current)
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
matchOffsets := []Offset{}
if t.merger.pattern != nil {
_, matchOffsets = t.merger.pattern.MatchItem(item)
}
var maxe int
for _, offset := range matchOffsets {
maxe = util.Max(maxe, int(offset[1]))
}
offsets := result.colorOffsets(matchOffsets, col2, bold, current)
maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
if overflow(text, maxWidth) {
if t.hscroll {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
if !overflow(text[:maxe], maxWidth-2) {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
if overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
@@ -857,7 +871,7 @@ func (t *Terminal) isPreviewEnabled() bool {
}
func (t *Terminal) current() string {
return t.merger.Get(t.cy).AsString(t.ansi)
return t.merger.Get(t.cy).item.AsString(t.ansi)
}
// Loop is called to start Terminal I/O
@@ -1028,13 +1042,13 @@ func (t *Terminal) Loop() {
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
item := t.merger.Get(y).item
if !selectItem(item) {
delete(t.selected, item.Index())
}
@@ -1059,14 +1073,14 @@ func (t *Terminal) Loop() {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
item := t.merger.Get(t.cy).item
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
}
case actExecuteMulti:
if len(t.selected) > 0 {
sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text)
sels[i] = quoteEntry(sel.text)
}
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else {
@@ -1128,7 +1142,7 @@ func (t *Terminal) Loop() {
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
item := t.merger.Get(i).item
selectItem(item)
}
req(reqList, reqInfo)

View File

@@ -18,9 +18,9 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text []rune
prefixLength int
trimLength int
text *util.Chars
prefixLength int32
trimLength int32
}
// Delimiter for tokenizing the input
@@ -75,15 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true
}
func withPrefixLengths(tokens [][]rune, begin int) []Token {
func withPrefixLengths(tokens []util.Chars, begin int) []Token {
ret := make([]Token, len(tokens))
prefixLength := begin
for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take
// the pointer to it
ret[idx] = Token{token, prefixLength, util.TrimLen(token)}
prefixLength += len(token)
// NOTE: &tokens[idx] instead of &tokens
ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())}
prefixLength += token.Length()
}
return ret
}
@@ -94,59 +93,60 @@ const (
awkWhite
)
func awkTokenizer(input []rune) ([][]rune, int) {
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
// 9, 32
ret := [][]rune{}
str := []rune{}
ret := []util.Chars{}
prefixLength := 0
state := awkNil
for _, r := range input {
numChars := input.Length()
begin := 0
end := 0
for idx := 0; idx < numChars; idx++ {
r := input.Get(idx)
white := r == 9 || r == 32
switch state {
case awkNil:
if white {
prefixLength++
} else {
state = awkBlack
str = append(str, r)
state, begin, end = awkBlack, idx, idx+1
}
case awkBlack:
str = append(str, r)
end = idx + 1
if white {
state = awkWhite
}
case awkWhite:
if white {
str = append(str, r)
end = idx + 1
} else {
ret = append(ret, str)
state = awkBlack
str = []rune{r}
ret = append(ret, input.Slice(begin, end))
state, begin, end = awkBlack, idx, idx+1
}
}
}
if len(str) > 0 {
ret = append(ret, str)
if begin < end {
ret = append(ret, input.Slice(begin, end))
}
return ret, prefixLength
}
// Tokenize tokenizes the given string with the delimiter
func Tokenize(runes []rune, delimiter Delimiter) []Token {
func Tokenize(text util.Chars, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(runes)
tokens, prefixLength := awkTokenizer(text)
return withPrefixLengths(tokens, prefixLength)
}
var tokens []string
if delimiter.str != nil {
tokens = strings.Split(string(runes), *delimiter.str)
for i := 0; i < len(tokens)-1; i++ {
tokens[i] = tokens[i] + *delimiter.str
return withPrefixLengths(text.Split(*delimiter.str), 0)
}
} else if delimiter.regex != nil {
str := string(runes)
// FIXME performance
var tokens []string
if delimiter.regex != nil {
str := text.ToString()
for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str)
if loc == nil {
@@ -157,9 +157,9 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
str = str[last:]
}
}
asRunes := make([][]rune, len(tokens))
asRunes := make([]util.Chars, len(tokens))
for i, token := range tokens {
asRunes[i] = []rune(token)
asRunes[i] = util.RunesToChars([]rune(token))
}
return withPrefixLengths(asRunes, 0)
}
@@ -167,15 +167,7 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
func joinTokens(tokens []Token) []rune {
ret := []rune{}
for _, token := range tokens {
ret = append(ret, token.text...)
}
return ret
}
func joinTokensAsRunes(tokens []Token) []rune {
ret := []rune{}
for _, token := range tokens {
ret = append(ret, token.text...)
ret = append(ret, token.text.ToRunes()...)
}
return ret
}
@@ -185,19 +177,20 @@ func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
for idx, r := range withNth {
part := []rune{}
parts := []*util.Chars{}
minIdx := 0
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
part = append(part, joinTokensAsRunes(tokens)...)
chars := util.RunesToChars(joinTokens(tokens))
parts = append(parts, &chars)
} else {
if idx < 0 {
idx += numTokens + 1
}
if idx >= 1 && idx <= numTokens {
minIdx = idx - 1
part = append(part, tokens[idx-1].text...)
parts = append(parts, tokens[idx-1].text)
}
}
} else {
@@ -224,17 +217,32 @@ func Transform(tokens []Token, withNth []Range) []Token {
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
part = append(part, tokens[idx-1].text...)
parts = append(parts, tokens[idx-1].text)
}
}
}
var prefixLength int
// Merge multiple parts
var merged util.Chars
switch len(parts) {
case 0:
merged = util.RunesToChars([]rune{})
case 1:
merged = *parts[0]
default:
runes := []rune{}
for _, part := range parts {
runes = append(runes, part.ToRunes()...)
}
merged = util.RunesToChars(runes)
}
var prefixLength int32
if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength
} else {
prefixLength = 0
}
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)}
transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())}
}
return transTokens
}

View File

@@ -1,6 +1,10 @@
package fzf
import "testing"
import (
"testing"
"github.com/junegunn/fzf/src/util"
)
func TestParseRange(t *testing.T) {
{
@@ -43,23 +47,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) {
// AWK-style
input := " abc: def: ghi "
tokens := Tokenize([]rune(input), Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens)
}
// With delimiter
tokens = Tokenize([]rune(input), delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens)
}
// With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
t.Errorf("%s", tokens)
}
}
@@ -67,7 +71,7 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl"
{
tokens := Tokenize([]rune(input), Delimiter{})
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
@@ -80,25 +84,25 @@ func TestTransform(t *testing.T) {
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", tx)
}
}
}
{
tokens := Tokenize([]rune(input), delimiterRegexp(":"))
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", tx)
}
}

View File

@@ -2,6 +2,7 @@
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
require 'rest_client'
require 'json'
if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>"

156
src/util/chars.go Normal file
View File

@@ -0,0 +1,156 @@
package util
import (
"unicode/utf8"
)
type Chars struct {
runes []rune
bytes []byte
}
// 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)
}
}
if ascii {
return Chars{bytes: bytea}
}
return Chars{runes: runes}
}
func RunesToChars(runes []rune) Chars {
return Chars{runes: runes}
}
func (chars *Chars) Get(i int) rune {
if chars.runes != nil {
return chars.runes[i]
}
return rune(chars.bytes[i])
}
func (chars *Chars) Length() int {
if chars.runes != nil {
return len(chars.runes)
}
return len(chars.bytes)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int {
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) ToString() string {
if chars.runes != nil {
return string(chars.runes)
}
return string(chars.bytes)
}
func (chars *Chars) ToRunes() []rune {
if chars.runes != nil {
return chars.runes
}
runes := make([]rune, len(chars.bytes))
for idx, b := range chars.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]}
}
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
}

82
src/util/chars_test.go Normal file
View File

@@ -0,0 +1,82 @@
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 {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
func TestCharsToString(t *testing.T) {
text := "\tabc한글 "
chars := ToChars([]byte(text))
if chars.ToString() != text {
t.Error()
}
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp int) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
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

@@ -4,21 +4,18 @@ package util
import "C"
import (
"math"
"os"
"os/exec"
"time"
"unicode/utf8"
)
// Max returns the largest integer
func Max(first int, items ...int) int {
max := first
for _, item := range items {
if item > max {
max = item
func Max(first int, second int) int {
if first >= second {
return first
}
}
return max
return second
}
// Min returns the smallest integer
@@ -67,6 +64,15 @@ func Constrain(val int, min int, max int) int {
return val
}
func AsUint16(val int) uint16 {
if val > math.MaxUint16 {
return math.MaxUint16
} else if val < 0 {
return 0
}
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration {
@@ -84,58 +90,6 @@ func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
}
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
return runes[0 : i+1]
}
// BytesToRunes converts byte array into rune array
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
}
// TrimLen returns the length of trimmed rune array
func TrimLen(runes []rune) int {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len(runes); j++ {
char := runes[j]
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")

View File

@@ -3,7 +3,7 @@ package util
import "testing"
func TestMax(t *testing.T) {
if Max(-2, 5, 1, 4, 3) != 5 {
if Max(-2, 5) != 5 {
t.Error("Invalid result")
}
}
@@ -20,23 +20,3 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3)
}
}
func TestTrimLen(t *testing.T) {
check := func(str string, exp int) {
trimmed := TrimLen([]rune(str))
if trimmed != exp {
t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}

View File

@@ -1,5 +1,6 @@
Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h')
unlet! g:fzf_layout g:fzf_action g:fzf_history_dir
Log 'Test directory: ' . g:dir
Save &acd
@@ -69,6 +70,79 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
" Working directory changed due to &acd
AssertEqual '/', getcwd()
Execute (fzf#wrap):
AssertThrows fzf#wrap({'foo': 'bar'})
let opts = fzf#wrap('foobar')
Log opts
AssertEqual '~40%', opts.down
Assert opts.options =~ '--expect='
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {}, 0)
Log opts
AssertEqual '~40%', opts.down
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'down': '50%'})
Log opts
AssertEqual '50%', opts.down
let opts = fzf#wrap('foobar', {'down': '50%'}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'options': '--reverse'})
Log opts
Assert opts.options =~ '--expect='
Assert opts.options =~ '--reverse'
let g:fzf_layout = {'window': 'enew'}
let opts = fzf#wrap('foobar')
Log opts
AssertEqual 'enew', opts.window
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'window')
let opts = fzf#wrap('foobar', {'right': '50%'})
Log opts
Assert !has_key(opts, 'window')
AssertEqual '50%', opts.right
let opts = fzf#wrap('foobar', {'right': '50%'}, 1)
Log opts
Assert !has_key(opts, 'window')
Assert !has_key(opts, 'right')
let g:fzf_action = {'a': 'tabe'}
let opts = fzf#wrap('foobar')
Log opts
Assert opts.options =~ '--expect=a'
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let g:fzf_history_dir = '/tmp'
let opts = fzf#wrap('foobar', {'options': '--color light'})
Log opts
Assert opts.options =~ '--history /tmp/foobar'
Assert opts.options =~ '--color light'
Execute (Cleanup):
unlet g:dir
Restore

View File

@@ -36,6 +36,10 @@ end
class Shell
class << self
def unsets
'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;'
end
def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end
@@ -45,6 +49,10 @@ class Shell
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end
def fish
'fish'
end
end
end
@@ -57,11 +65,11 @@ class Tmux
@win =
case shell
when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first
go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first
when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first
when :fish
go("new-window -d -P -F '#I' 'fish'").first
go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first
else
raise "Unknown shell: #{shell}"
end
@@ -153,12 +161,6 @@ class TestBase < Minitest::Test
@temp_suffix].join '-'
end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def readonce
wait { File.exists?(tempname) }
File.read(tempname)