mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-16 07:13:48 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dbc108da2 | ||
|
|
bd98f988f0 | ||
|
|
06301c7847 | ||
|
|
18a1aeaa91 | ||
|
|
c9f16b6430 | ||
|
|
bc9d2abdb6 | ||
|
|
28810c178f | ||
|
|
a9e64efe45 | ||
|
|
6b5886c034 | ||
|
|
7727ad43af | ||
|
|
bbe10f4f77 | ||
|
|
5e72709613 | ||
|
|
9e85cba0d0 | ||
|
|
4b59ced08f | ||
|
|
8dbdd55730 | ||
|
|
6725151a99 | ||
|
|
d4f3d5a164 | ||
|
|
7b5ccc45bc | ||
|
|
940214a1a2 | ||
|
|
68bd410159 | ||
|
|
b13fcfd831 | ||
|
|
07ef2b051c | ||
|
|
3fc795340d | ||
|
|
70cfa6af13 | ||
|
|
dbcaec59ae | ||
|
|
faedae708e | ||
|
|
0c66521b23 | ||
|
|
bf92862459 | ||
|
|
1a68698d76 | ||
|
|
842a73357c | ||
|
|
5efdeccdbb | ||
|
|
050777b8c4 | ||
|
|
a4d78e2200 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
gopath
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,13 +1,30 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.16.10
|
||||
-------
|
||||
- Fixed invalid handling of ANSI colors in preview window
|
||||
- Further improved `--ansi` performance
|
||||
|
||||
0.16.9
|
||||
------
|
||||
- Memory and performance optimization
|
||||
- Around 20% performance improvement for general use cases
|
||||
- Up to 5x faster processing of `--ansi`
|
||||
- Up to 50% reduction of memory usage
|
||||
- Bug fixes and usability improvements
|
||||
- Fixed handling of bracketed paste mode
|
||||
- [ERROR] on info line when the default command failed
|
||||
- More efficient rendering of preview window
|
||||
- `--no-clear` updated for repetitive relaunching scenarios
|
||||
|
||||
0.16.8
|
||||
------
|
||||
- New `change` event and `top` action for `--bind`
|
||||
- `fzf --bind change:top`
|
||||
- Move cursor to the top result whenever the query string is changed
|
||||
- `fzf --bind ctrl-u:unix-word-rubout+top`
|
||||
- `top` combined with `unix-word-rubout`
|
||||
- `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'`
|
||||
- `top` combined with `unix-word-rubout` and `unix-line-discard`
|
||||
- Fixed inconsistent tiebreak scores when `--nth` is used
|
||||
- Proper display of tab characters in `--prompt`
|
||||
- Fixed not to `--cycle` on page-up/page-down to prevent overshoot
|
||||
|
||||
@@ -457,7 +457,7 @@ make use of this feature. `$dir` defaults to `.` when the last token is not a
|
||||
valid directory. Example:
|
||||
|
||||
```sh
|
||||
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
```
|
||||
|
||||
[License](LICENSE)
|
||||
|
||||
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.16.8
|
||||
version=0.16.10
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Jun 2017" "fzf 0.16.8" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Jul 2017" "fzf 0.16.10" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jun 2017" "fzf 0.16.8" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Jul 2017" "fzf 0.16.10" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -236,6 +236,7 @@ e.g. \fBfzf --color=bg+:24\fR
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBinfo \fRInfo
|
||||
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
|
||||
@@ -149,13 +149,8 @@ function! s:tmux_enabled()
|
||||
endfunction
|
||||
|
||||
function! s:escape(path)
|
||||
let escaped_chars = '$%#''"'
|
||||
|
||||
if has('unix')
|
||||
let escaped_chars .= ' \'
|
||||
endif
|
||||
|
||||
return escape(a:path, escaped_chars)
|
||||
let path = fnameescape(a:path)
|
||||
return s:is_win ? escape(path, '$') : path
|
||||
endfunction
|
||||
|
||||
" Upgrade legacy options
|
||||
@@ -361,11 +356,14 @@ try
|
||||
if has('nvim') && !has_key(dict, 'dir')
|
||||
let dict.dir = s:fzf_getcwd()
|
||||
endif
|
||||
if has('win32unix') && has_key(dict, 'dir')
|
||||
let dict.dir = fnamemodify(dict.dir, ':p')
|
||||
endif
|
||||
|
||||
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
|
||||
let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '')
|
||||
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
|
||||
let temps.source = s:fzf_tempname()
|
||||
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
|
||||
let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(temps.source)
|
||||
let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source)
|
||||
endif
|
||||
|
||||
if has_key(dict, 'source')
|
||||
@@ -526,12 +524,15 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
let command = batchfile
|
||||
let a:temps.batchfile = batchfile
|
||||
if has('nvim')
|
||||
let s:dict = a:dict
|
||||
let s:temps = a:temps
|
||||
let fzf = {}
|
||||
let fzf.dict = a:dict
|
||||
let fzf.temps = a:temps
|
||||
function! fzf.on_exit(job_id, exit_status, event) dict
|
||||
let lines = s:collect(s:temps)
|
||||
call s:callback(s:dict, lines)
|
||||
if s:present(self.dict, 'dir')
|
||||
execute 'lcd' s:escape(self.dict.dir)
|
||||
endif
|
||||
let lines = s:collect(self.temps)
|
||||
call s:callback(self.dict, lines)
|
||||
endfunction
|
||||
let cmd = 'start /wait cmd /c '.command
|
||||
call jobstart(cmd, fzf)
|
||||
@@ -765,8 +766,6 @@ function! s:cmd(bang, ...) abort
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
|
||||
elseif has('win32unix')
|
||||
let opts.dir = fnamemodify(opts.dir, ':p')
|
||||
endif
|
||||
let prompt = opts.dir
|
||||
else
|
||||
|
||||
@@ -4,14 +4,9 @@ function fzf_key_bindings
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l dir (commandline -t)
|
||||
# The commandline token might be escaped, we need to unescape it.
|
||||
set dir (eval "printf '%s' $dir")
|
||||
if [ ! -d "$dir" ]
|
||||
set dir .
|
||||
end
|
||||
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
|
||||
set dir (string replace --regex '(.)/+$' '$1' "$dir")
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
@@ -19,19 +14,17 @@ function fzf_key_bindings
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
end
|
||||
|
||||
if [ "$dir" != . ]
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
@@ -46,22 +39,45 @@ function fzf_key_bindings
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
|
||||
set -l FISH_MAJOR (echo $FISH_VERSION | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $FISH_VERSION | cut -f2 -d.)
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
|
||||
[ "$result" ]; and cd $result
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
@@ -84,4 +100,47 @@ function fzf_key_bindings
|
||||
bind -M insert \cr fzf-history-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
||||
# eval is used to do shell expansion on paths
|
||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ]
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" '' "$commandline")
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' '' $dir)
|
||||
end
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while [ ! -d "$dir" ]
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname "$dir")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -283,8 +283,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
||||
|
||||
// Phase 1. Check if there's a match and calculate bonus for each point
|
||||
pidx, lastIdx, prevClass := 0, 0, charNonWord
|
||||
input.CopyRunes(T)
|
||||
for idx := 0; idx < N; idx++ {
|
||||
char := input.Get(idx)
|
||||
char := T[idx]
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
@@ -389,7 +390,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(F[i]); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(input.Get(j)) + " ")
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
|
||||
if !caseSensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
|
||||
res, pos := fun(caseSensitive, normalize, forward, util.ToChars([]byte(input)), []rune(pattern), true, nil)
|
||||
var start, end int
|
||||
if pos == nil || len(*pos) == 0 {
|
||||
start = res.Start
|
||||
|
||||
60
src/ansi.go
60
src/ansi.go
@@ -44,7 +44,21 @@ func init() {
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08")
|
||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
}
|
||||
|
||||
func findAnsiStart(str string) int {
|
||||
idx := 0
|
||||
for ; idx < len(str); idx++ {
|
||||
b := str[idx]
|
||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
||||
return idx
|
||||
}
|
||||
if b == 0x08 && idx > 0 {
|
||||
return idx - 1
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
@@ -55,41 +69,61 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
|
||||
prev := str[idx:offset[0]]
|
||||
output.WriteString(prev)
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
|
||||
// No sign of ANSI code
|
||||
if idx == len(str) {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure that we found an ANSI code
|
||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
||||
if offset == nil {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
offset[0] += idx
|
||||
offset[1] += idx
|
||||
idx = offset[1]
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
|
||||
prevIdx = offset[1]
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
output.WriteString(prev)
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
if !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
newLen := int32(utf8.RuneCount(output.Bytes()))
|
||||
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
}
|
||||
}
|
||||
|
||||
idx = offset[1]
|
||||
}
|
||||
|
||||
rest := str[idx:]
|
||||
rest := str[prevIdx:]
|
||||
if len(rest) > 0 {
|
||||
output.WriteString(rest)
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
}
|
||||
if proc != nil {
|
||||
|
||||
42
src/cache.go
42
src/cache.go
@@ -3,7 +3,7 @@ package fzf
|
||||
import "sync"
|
||||
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]*Result
|
||||
type queryCache map[string][]Result
|
||||
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
type ChunkCache struct {
|
||||
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
return
|
||||
}
|
||||
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
||||
(*qc)[key] = list
|
||||
}
|
||||
|
||||
// Find is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
|
||||
// Lookup is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
@@ -46,8 +46,36 @@ func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
|
||||
if ok {
|
||||
list, ok := (*qc)[key]
|
||||
if ok {
|
||||
return list, true
|
||||
return list
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx := 1; idx < len(key); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
// [--------| ] | [ |--------]
|
||||
// [-------| ] | [ |-------]
|
||||
prefix := key[:len(key)-idx]
|
||||
suffix := key[idx:]
|
||||
for _, substr := range [2]string{prefix, suffix} {
|
||||
if cached, found := (*qc)[substr]; found {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,34 +7,34 @@ func TestChunkCache(t *testing.T) {
|
||||
chunk2 := make(Chunk, chunkSize)
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &chunk2
|
||||
items1 := []*Result{&Result{}}
|
||||
items2 := []*Result{&Result{}, &Result{}}
|
||||
items1 := []Result{Result{}}
|
||||
items2 := []Result{Result{}, Result{}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
||||
{ // chunk1 is not full
|
||||
cached, found := cache.Find(chunk1p, "foo")
|
||||
if found {
|
||||
t.Error("Cached disabled for non-empty chunks", found, cached)
|
||||
cached := cache.Lookup(chunk1p, "foo")
|
||||
if cached != nil {
|
||||
t.Error("Cached disabled for non-empty chunks", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached, found := cache.Find(chunk2p, "foo")
|
||||
if !found || len(cached) != 1 {
|
||||
t.Error("Expected 1 item cached", found, cached)
|
||||
cached := cache.Lookup(chunk2p, "foo")
|
||||
if cached == nil || len(cached) != 1 {
|
||||
t.Error("Expected 1 item cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached, found := cache.Find(chunk2p, "bar")
|
||||
if !found || len(cached) != 2 {
|
||||
t.Error("Expected 2 items cached", found, cached)
|
||||
cached := cache.Lookup(chunk2p, "bar")
|
||||
if cached == nil || len(cached) != 2 {
|
||||
t.Error("Expected 2 items cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached, found := cache.Find(chunk1p, "foobar")
|
||||
if found {
|
||||
t.Error("Expected 0 item cached", found, cached)
|
||||
cached := cache.Lookup(chunk1p, "foobar")
|
||||
if cached != nil {
|
||||
t.Error("Expected 0 item cached", cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
|
||||
type Chunk []*Item // >>> []Item
|
||||
// Chunk is a list of Items whose size has the upper limit of chunkSize
|
||||
type Chunk []Item
|
||||
|
||||
// ItemBuilder is a closure type that builds Item object from a pointer to a
|
||||
// string and an integer
|
||||
type ItemBuilder func([]byte, int) *Item
|
||||
type ItemBuilder func([]byte, int) Item
|
||||
|
||||
// ChunkList is a list of Chunks
|
||||
type ChunkList struct {
|
||||
@@ -28,11 +28,11 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
|
||||
item := trans(data, index)
|
||||
if item != nil {
|
||||
*c = append(*c, item)
|
||||
return true
|
||||
if item.Nil() {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
*c = append(*c, item)
|
||||
return true
|
||||
}
|
||||
|
||||
// IsFull returns true if the Chunk is full
|
||||
@@ -58,7 +58,7 @@ func (cl *ChunkList) Push(data []byte) bool {
|
||||
defer cl.mutex.Unlock()
|
||||
|
||||
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
|
||||
newChunk := Chunk(make([]*Item, 0, chunkSize))
|
||||
newChunk := Chunk(make([]Item, 0, chunkSize))
|
||||
cl.chunks = append(cl.chunks, &newChunk)
|
||||
}
|
||||
|
||||
@@ -79,15 +79,8 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
|
||||
// Duplicate the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
ret[cnt-1] = ret[cnt-1].dupe()
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
return ret, cl.count
|
||||
}
|
||||
|
||||
func (c *Chunk) dupe() *Chunk {
|
||||
newChunk := make(Chunk, len(*c))
|
||||
for idx, ptr := range *c {
|
||||
newChunk[idx] = ptr
|
||||
}
|
||||
return &newChunk
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(s []byte, i int) *Item {
|
||||
return &Item{text: util.ToChars(s), index: int32(i * 2)}
|
||||
cl := NewChunkList(func(s []byte, i int) Item {
|
||||
chars := util.ToChars(s)
|
||||
chars.Index = int32(i * 2)
|
||||
return Item{text: chars}
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
@@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) {
|
||||
if len(*chunk1) != 2 {
|
||||
t.Error("Snapshot should contain only two items")
|
||||
}
|
||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
|
||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
|
||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 ||
|
||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 {
|
||||
t.Error("Invalid data")
|
||||
}
|
||||
if chunk1.IsFull() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
const (
|
||||
// Current version
|
||||
version = "0.16.8"
|
||||
version = "0.16.10"
|
||||
|
||||
// Core
|
||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||
|
||||
50
src/core.go
50
src/core.go
@@ -63,67 +63,51 @@ func Run(opts *Options, revision string) {
|
||||
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
return util.ToChars(data), nil
|
||||
}
|
||||
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
|
||||
return util.RunesToChars(data), nil
|
||||
}
|
||||
if opts.Ansi {
|
||||
if opts.Theme != nil {
|
||||
var state *ansiState
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, offsets, newState := extractColor(string(data), state, nil)
|
||||
state = newState
|
||||
return util.RunesToChars([]rune(trimmed)), offsets
|
||||
return util.ToChars([]byte(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(string(data), nil, nil)
|
||||
return util.RunesToChars([]rune(trimmed)), nil
|
||||
return util.ToChars([]byte(trimmed)), nil
|
||||
}
|
||||
}
|
||||
ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
|
||||
return ansiProcessor([]byte(string(data)))
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
var chunkList *ChunkList
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
||||
chunkList = NewChunkList(func(data []byte, index int) Item {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return nil
|
||||
return nilItem
|
||||
}
|
||||
chars, colors := ansiProcessor(data)
|
||||
return &Item{
|
||||
index: int32(index),
|
||||
trimLength: -1,
|
||||
text: chars,
|
||||
colors: colors}
|
||||
chars.Index = int32(index)
|
||||
return Item{text: chars, colors: colors}
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
||||
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
|
||||
chunkList = NewChunkList(func(data []byte, index int) Item {
|
||||
tokens := Tokenize(string(data), opts.Delimiter)
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(joinTokens(trans)))
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return nil
|
||||
return nilItem
|
||||
}
|
||||
textRunes := joinTokens(trans)
|
||||
item := Item{
|
||||
index: int32(index),
|
||||
trimLength: -1,
|
||||
origText: &data,
|
||||
colors: nil}
|
||||
|
||||
trimmed, colors := ansiProcessorRunes(textRunes)
|
||||
item.text = trimmed
|
||||
item.colors = colors
|
||||
return &item
|
||||
trimmed, colors := ansiProcessor([]byte(transformed))
|
||||
trimmed.Index = int32(index)
|
||||
return Item{text: trimmed, colors: colors, origText: &data}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -168,8 +152,8 @@ func Run(opts *Options, revision string) {
|
||||
reader := Reader{
|
||||
func(runes []byte) bool {
|
||||
item := chunkList.trans(runes, 0)
|
||||
if item != nil {
|
||||
if result, _, _ := pattern.MatchItem(item, false, slab); result != nil {
|
||||
if !item.Nil() {
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
found = true
|
||||
}
|
||||
@@ -228,7 +212,7 @@ func Run(opts *Options, revision string) {
|
||||
case EvtReadNew, EvtReadFin:
|
||||
reading = reading && evt == EvtReadNew
|
||||
snapshot, count := chunkList.Snapshot()
|
||||
terminal.UpdateCount(count, !reading)
|
||||
terminal.UpdateCount(count, !reading, value.(bool))
|
||||
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
|
||||
|
||||
case EvtSearchNew:
|
||||
|
||||
28
src/item.go
28
src/item.go
@@ -4,27 +4,27 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Item represents each input line
|
||||
// Item represents each input line. 56 bytes.
|
||||
type Item struct {
|
||||
index int32
|
||||
trimLength int32
|
||||
text util.Chars
|
||||
origText *[]byte
|
||||
colors *[]ansiOffset
|
||||
transformed []Token
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (item *Item) Index() int32 {
|
||||
return item.index
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
func (item *Item) TrimLength() int32 {
|
||||
if item.trimLength >= 0 {
|
||||
return item.trimLength
|
||||
}
|
||||
item.trimLength = int32(item.text.TrimLength())
|
||||
return item.trimLength
|
||||
var nilItem = Item{text: util.Chars{Index: -1}}
|
||||
|
||||
func (item *Item) Nil() bool {
|
||||
return item.Index() < 0
|
||||
}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
}
|
||||
|
||||
// Colors returns ansiOffsets of the Item
|
||||
|
||||
@@ -131,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
|
||||
type partialResult struct {
|
||||
index int
|
||||
matches []*Result
|
||||
matches []Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
@@ -162,7 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
||||
defer func() { waitGroup.Done() }()
|
||||
count := 0
|
||||
allMatches := make([][]*Result, len(chunks))
|
||||
allMatches := make([][]Result, len(chunks))
|
||||
for idx, chunk := range chunks {
|
||||
matches := request.pattern.Match(chunk, slab)
|
||||
allMatches[idx] = matches
|
||||
@@ -172,7 +172,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
countChan <- len(matches)
|
||||
}
|
||||
sliceMatches := make([]*Result, 0, count)
|
||||
sliceMatches := make([]Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]*Result, numSlices)
|
||||
partialResults := make([][]Result, numSlices)
|
||||
for _ = range slices {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
|
||||
@@ -3,14 +3,14 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]*Result
|
||||
merged []*Result
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
@@ -35,11 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []*Result{},
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
@@ -59,13 +59,13 @@ func (mg *Merger) Length() int {
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) *Result {
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return &Result{item: (*chunk)[idx%chunkSize]}
|
||||
return Result{item: &(*chunk)[idx%chunkSize]}
|
||||
}
|
||||
|
||||
if mg.sorted {
|
||||
@@ -89,7 +89,7 @@ func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
func (mg *Merger) mergedGet(idx int) *Result {
|
||||
func (mg *Merger) mergedGet(idx int) Result {
|
||||
for i := len(mg.merged); i <= idx; i++ {
|
||||
minRank := minRank()
|
||||
minIdx := -1
|
||||
@@ -100,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Result {
|
||||
continue
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor].rank
|
||||
rank := list[cursor]
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
|
||||
@@ -15,11 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func randResult() *Result {
|
||||
func randResult() Result {
|
||||
str := fmt.Sprintf("%d", rand.Uint32())
|
||||
return &Result{
|
||||
item: &Item{text: util.RunesToChars([]rune(str))},
|
||||
rank: rank{index: rand.Int31()}}
|
||||
chars := util.ToChars([]byte(str))
|
||||
chars.Index = rand.Int31()
|
||||
return Result{item: &Item{text: chars}}
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
@@ -29,14 +29,14 @@ func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
numLists := 4
|
||||
lists := make([][]*Result, numLists)
|
||||
lists := make([][]Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]*Result, numResults)
|
||||
lists[i] = make([]Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
@@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
||||
sort.Sort(ByRelevance(lists[i]))
|
||||
}
|
||||
}
|
||||
items := []*Result{}
|
||||
items := []Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
|
||||
@@ -963,6 +963,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
|
||||
case "--expect":
|
||||
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
|
||||
case "--no-expect":
|
||||
opts.Expect = make(map[int]string)
|
||||
case "--tiebreak":
|
||||
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||
case "--bind":
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
@@ -44,7 +43,7 @@ func TestDelimiterRegex(t *testing.T) {
|
||||
|
||||
func TestDelimiterRegexString(t *testing.T) {
|
||||
delim := delimiterRegexp("*")
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.regex != nil ||
|
||||
tokens[0].text.ToString() != "-*" ||
|
||||
tokens[1].text.ToString() != "--*" ||
|
||||
@@ -57,7 +56,7 @@ func TestDelimiterRegexString(t *testing.T) {
|
||||
|
||||
func TestDelimiterRegexRegex(t *testing.T) {
|
||||
delim := delimiterRegexp("--\\*")
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.str != nil ||
|
||||
tokens[0].text.ToString() != "-*--*" ||
|
||||
tokens[1].text.ToString() != "---*" ||
|
||||
|
||||
@@ -243,31 +243,17 @@ func (p *Pattern) CacheKey() string {
|
||||
}
|
||||
|
||||
// Match returns the list of matches Items in the given Chunk
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result {
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
if p.cacheable {
|
||||
if cached, found := _cache.Find(chunk, cacheKey); found {
|
||||
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix/suffix cache
|
||||
var space []*Result
|
||||
Loop:
|
||||
for idx := 1; idx < len(cacheKey); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
// [--------| ] | [ |--------]
|
||||
// [-------| ] | [ |-------]
|
||||
prefix := cacheKey[:len(cacheKey)-idx]
|
||||
suffix := cacheKey[idx:]
|
||||
for _, substr := range [2]*string{&prefix, &suffix} {
|
||||
if cached, found := _cache.Find(chunk, *substr); found {
|
||||
space = cached
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
space := _cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
|
||||
@@ -277,19 +263,19 @@ Loop:
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result {
|
||||
matches := []*Result{}
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if space == nil {
|
||||
for _, item := range *chunk {
|
||||
if match, _, _ := p.MatchItem(item, false, slab); match != nil {
|
||||
matches = append(matches, match)
|
||||
for idx := range *chunk {
|
||||
if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
|
||||
matches = append(matches, match)
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,14 +286,16 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||
if p.extended {
|
||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
offsets := []Offset{offset}
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
@@ -366,18 +354,17 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
}
|
||||
|
||||
func (p *Pattern) prepareInput(item *Item) []Token {
|
||||
if item.transformed != nil {
|
||||
return item.transformed
|
||||
if len(p.nth) == 0 {
|
||||
return []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
}
|
||||
|
||||
var ret []Token
|
||||
if len(p.nth) == 0 {
|
||||
ret = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
tokens := Tokenize(item.text, p.delimiter)
|
||||
ret = Transform(tokens, p.nth)
|
||||
if item.transformed != nil {
|
||||
return *item.transformed
|
||||
}
|
||||
item.transformed = ret
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
item.transformed = &ret
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestExact(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != 7 || res.End != 10 {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestEqual(t *testing.T) {
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
res, pos := algo.EqualMatch(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte(str)), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != sidxExpected || res.End != eidxExpected {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -133,30 +133,30 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
chunk := Chunk{
|
||||
&Item{
|
||||
text: util.RunesToChars([]rune("junegunn")),
|
||||
Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: trans},
|
||||
transformed: &trans},
|
||||
}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||
reflect.DeepEqual(matches[0].item.transformed, trans)) {
|
||||
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
match, offsets, pos := pattern.MatchItem(chunk[0], true, slab)
|
||||
match, offsets, pos := pattern.MatchItem(&chunk[0], true, slab)
|
||||
if !(match.item.text.ToString() == "junegunn" &&
|
||||
string(*match.item.origText) == "junegunn.choi" &&
|
||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||
reflect.DeepEqual(match.item.transformed, trans)) {
|
||||
reflect.DeepEqual(*match.item.transformed, trans)) {
|
||||
t.Error("Invalid match result", match, offsets, extended)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
|
||||
@@ -17,16 +17,17 @@ type Reader struct {
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
cmd = defaultCommand
|
||||
}
|
||||
r.readFromCommand(cmd)
|
||||
success = r.readFromCommand(cmd)
|
||||
} else {
|
||||
r.readFromStdin()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.eventBox.Set(EvtReadFin, nil)
|
||||
r.eventBox.Set(EvtReadFin, success)
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
@@ -50,7 +51,7 @@ func (r *Reader) feed(src io.Reader) {
|
||||
}
|
||||
}
|
||||
if r.pusher(bytea) {
|
||||
r.eventBox.Set(EvtReadNew, nil)
|
||||
r.eventBox.Set(EvtReadNew, true)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -59,20 +60,21 @@ func (r *Reader) feed(src io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) readFromStdin() {
|
||||
func (r *Reader) readFromStdin() bool {
|
||||
r.feed(os.Stdin)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(cmd string) {
|
||||
func (r *Reader) readFromCommand(cmd string) bool {
|
||||
listCommand := util.ExecCommand(cmd)
|
||||
out, err := listCommand.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
err = listCommand.Start()
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
defer listCommand.Wait()
|
||||
r.feed(out)
|
||||
return listCommand.Wait() == nil
|
||||
}
|
||||
|
||||
@@ -19,22 +19,17 @@ type colorOffset struct {
|
||||
index int32
|
||||
}
|
||||
|
||||
type rank struct {
|
||||
points [4]uint16
|
||||
index int32
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
rank rank
|
||||
item *Item
|
||||
points [4]uint16
|
||||
}
|
||||
|
||||
func buildResult(item *Item, offsets []Offset, score int) *Result {
|
||||
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if len(offsets) > 1 {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
}
|
||||
|
||||
result := Result{item: item, rank: rank{index: item.index}}
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
@@ -57,7 +52,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byLength:
|
||||
val = util.AsUint16(int(item.TrimLength()))
|
||||
val = item.TrimLength()
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
@@ -75,10 +70,10 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
|
||||
}
|
||||
}
|
||||
}
|
||||
result.rank.points[idx] = val
|
||||
result.points[idx] = val
|
||||
}
|
||||
|
||||
return &result
|
||||
return result
|
||||
}
|
||||
|
||||
// Sort criteria to use. Never changes once fzf is started.
|
||||
@@ -86,11 +81,11 @@ var sortCriteria []criterion
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (result *Result) Index() int32 {
|
||||
return result.item.index
|
||||
return result.item.Index()
|
||||
}
|
||||
|
||||
func minRank() rank {
|
||||
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
func minRank() Result {
|
||||
return Result{item: &nilItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
|
||||
@@ -201,7 +196,7 @@ func (a ByOrder) Less(i, j int) bool {
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
type ByRelevance []*Result
|
||||
type ByRelevance []Result
|
||||
|
||||
func (a ByRelevance) Len() int {
|
||||
return len(a)
|
||||
@@ -212,11 +207,11 @@ func (a ByRelevance) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (a ByRelevance) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
|
||||
return compareRanks(a[i], a[j], false)
|
||||
}
|
||||
|
||||
// ByRelevanceTac is for sorting Items
|
||||
type ByRelevanceTac []*Result
|
||||
type ByRelevanceTac []Result
|
||||
|
||||
func (a ByRelevanceTac) Len() int {
|
||||
return len(a)
|
||||
@@ -227,10 +222,10 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
|
||||
return compareRanks(a[i], a[j], true)
|
||||
}
|
||||
|
||||
func compareRanks(irank rank, jrank rank, tac bool) bool {
|
||||
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
for idx := 0; idx < 4; idx++ {
|
||||
left := irank.points[idx]
|
||||
right := jrank.points[idx]
|
||||
@@ -240,5 +235,5 @@ func compareRanks(irank rank, jrank rank, tac bool) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (irank.index <= jrank.index) != tac
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func withIndex(i *Item, index int) *Item {
|
||||
(*i).text.Index = int32(index)
|
||||
return i
|
||||
}
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
@@ -26,10 +31,10 @@ func TestOffsetSort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRankComparison(t *testing.T) {
|
||||
rank := func(vals ...uint16) rank {
|
||||
return rank{
|
||||
rank := func(vals ...uint16) Result {
|
||||
return Result{
|
||||
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
|
||||
index: int32(vals[4])}
|
||||
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
|
||||
}
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
@@ -52,36 +57,41 @@ func TestResultRank(t *testing.T) {
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2)
|
||||
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus
|
||||
item1.rank.points[1] != 3 || // Length
|
||||
item1.rank.points[2] != 0 || // Unused
|
||||
item1.rank.points[3] != 0 || // Unused
|
||||
item1.item.index != 1 {
|
||||
t.Error(item1.rank)
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
if item1.points[0] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[1] != 3 || // Length
|
||||
item1.points[2] != 0 || // Unused
|
||||
item1.points[3] != 0 || // Unused
|
||||
item1.item.Index() != 1 {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
|
||||
items := []*Result{item1, item2}
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
items = []*Result{item2, item1, item1, item2}
|
||||
items = []Result{item2, item1, item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item2 ||
|
||||
items[2] != item1 || items[3] != item1 {
|
||||
t.Error(items, item1, item1.item.index, item2, item2.item.index)
|
||||
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
items = []*Result{item1, item2, item3, item4, item5, item6}
|
||||
item3 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
item4 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
item5 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
item6 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if !(items[0] == item6 && items[1] == item5 &&
|
||||
items[2] == item4 && items[3] == item3 &&
|
||||
|
||||
@@ -87,6 +87,7 @@ type Terminal struct {
|
||||
margin [4]sizeSpec
|
||||
strong tui.Attr
|
||||
bordered bool
|
||||
cleanExit bool
|
||||
border tui.Window
|
||||
window tui.Window
|
||||
pborder tui.Window
|
||||
@@ -94,6 +95,7 @@ type Terminal struct {
|
||||
count int
|
||||
progress int
|
||||
reading bool
|
||||
success bool
|
||||
jumping jumpMode
|
||||
jumpLabels string
|
||||
printer func(string)
|
||||
@@ -365,6 +367,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
history: opts.History,
|
||||
margin: opts.Margin,
|
||||
bordered: opts.Bordered,
|
||||
cleanExit: opts.ClearOnExit,
|
||||
strong: strongAttr,
|
||||
cycle: opts.Cycle,
|
||||
header: header,
|
||||
@@ -372,6 +375,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
reading: true,
|
||||
success: true,
|
||||
jumping: jumpDisabled,
|
||||
jumpLabels: opts.JumpLabels,
|
||||
printer: opts.Printer,
|
||||
@@ -401,10 +405,11 @@ func (t *Terminal) Input() []rune {
|
||||
}
|
||||
|
||||
// UpdateCount updates the count information
|
||||
func (t *Terminal) UpdateCount(cnt int, final bool) {
|
||||
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
|
||||
t.mutex.Lock()
|
||||
t.count = cnt
|
||||
t.reading = !final
|
||||
t.success = success
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
if final {
|
||||
@@ -682,6 +687,9 @@ func (t *Terminal) printInfo() {
|
||||
if t.progress > 0 && t.progress < 100 {
|
||||
output += fmt.Sprintf(" (%d%%)", t.progress)
|
||||
}
|
||||
if !t.success && t.count == 0 {
|
||||
output += " [ERROR]"
|
||||
}
|
||||
if pos+len(output) <= t.window.Width() {
|
||||
t.window.CPrint(tui.ColInfo, 0, output)
|
||||
}
|
||||
@@ -704,11 +712,11 @@ func (t *Terminal) printHeader() {
|
||||
trimmed, colors, newState := extractColor(lineStr, state, nil)
|
||||
state = newState
|
||||
item := &Item{
|
||||
text: util.RunesToChars([]rune(trimmed)),
|
||||
text: util.ToChars([]byte(trimmed)),
|
||||
colors: colors}
|
||||
|
||||
t.move(line, 2, true)
|
||||
t.printHighlighted(&Result{item: item},
|
||||
t.printHighlighted(Result{item: item},
|
||||
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false)
|
||||
}
|
||||
}
|
||||
@@ -736,7 +744,7 @@ func (t *Terminal) printList() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
|
||||
func (t *Terminal) printItem(result Result, line int, i int, current bool) {
|
||||
item := result.item
|
||||
_, selected := t.selected[item.Index()]
|
||||
label := " "
|
||||
@@ -752,7 +760,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
|
||||
|
||||
// Avoid unnecessary redraw
|
||||
newLine := itemLine{current: current, selected: selected, label: label,
|
||||
result: *result, queryLen: len(t.input), width: 0}
|
||||
result: result, queryLen: len(t.input), width: 0}
|
||||
prevLine := t.prevLines[i]
|
||||
if prevLine.current == newLine.current &&
|
||||
prevLine.selected == newLine.selected &&
|
||||
@@ -834,7 +842,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
|
||||
return t.displayWidthWithLimit(runes, 0, max) > max
|
||||
}
|
||||
|
||||
func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
|
||||
func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
|
||||
item := result.item
|
||||
|
||||
// Overflow
|
||||
@@ -954,6 +962,7 @@ func (t *Terminal) printPreview() {
|
||||
}
|
||||
reader := bufio.NewReader(strings.NewReader(t.previewer.text))
|
||||
lineNo := -t.previewer.offset
|
||||
height := t.pwindow.Height()
|
||||
var ansi *ansiState
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
@@ -962,7 +971,8 @@ func (t *Terminal) printPreview() {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
lineNo++
|
||||
if lineNo > t.pwindow.Height() {
|
||||
if lineNo > height ||
|
||||
t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||
break
|
||||
} else if lineNo > 0 {
|
||||
var fillRet tui.FillReturn
|
||||
@@ -992,7 +1002,7 @@ func (t *Terminal) printPreview() {
|
||||
}
|
||||
}
|
||||
t.pwindow.FinishFill()
|
||||
if t.previewer.lines > t.pwindow.Height() {
|
||||
if t.previewer.lines > height {
|
||||
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
|
||||
pos := t.pwindow.Width() - len(offset)
|
||||
if t.tui.DoesAutoWrap() {
|
||||
@@ -1163,8 +1173,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
|
||||
}
|
||||
|
||||
for idx, item := range items {
|
||||
chars := util.RunesToChars([]rune(item.AsString(stripAnsi)))
|
||||
tokens := Tokenize(chars, delimiter)
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := string(joinTokens(trans))
|
||||
if delimiter.str != nil {
|
||||
@@ -1335,7 +1344,12 @@ func (t *Terminal) Loop() {
|
||||
}()
|
||||
}
|
||||
|
||||
exit := func(code int) {
|
||||
exit := func(getCode func() int) {
|
||||
if !t.cleanExit && t.fullscreen && t.inlineInfo {
|
||||
t.placeCursor()
|
||||
}
|
||||
t.tui.Close()
|
||||
code := getCode()
|
||||
if code <= exitNoMatch && t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
@@ -1383,11 +1397,12 @@ func (t *Terminal) Loop() {
|
||||
case reqRedraw:
|
||||
t.redraw()
|
||||
case reqClose:
|
||||
t.tui.Close()
|
||||
if t.output() {
|
||||
exit(exitOk)
|
||||
}
|
||||
exit(exitNoMatch)
|
||||
exit(func() int {
|
||||
if t.output() {
|
||||
return exitOk
|
||||
}
|
||||
return exitNoMatch
|
||||
})
|
||||
case reqPreviewDisplay:
|
||||
t.previewer.text = value.(string)
|
||||
t.previewer.lines = strings.Count(t.previewer.text, "\n")
|
||||
@@ -1396,12 +1411,12 @@ func (t *Terminal) Loop() {
|
||||
case reqPreviewRefresh:
|
||||
t.printPreview()
|
||||
case reqPrintQuery:
|
||||
t.tui.Close()
|
||||
t.printer(string(t.input))
|
||||
exit(exitOk)
|
||||
exit(func() int {
|
||||
t.printer(string(t.input))
|
||||
return exitOk
|
||||
})
|
||||
case reqQuit:
|
||||
t.tui.Close()
|
||||
exit(exitInterrupt)
|
||||
exit(func() int { return exitInterrupt })
|
||||
}
|
||||
}
|
||||
t.placeCursor()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func newItem(str string) *Item {
|
||||
bytes := []byte(str)
|
||||
trimmed, _, _ := extractColor(str, nil, nil)
|
||||
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -74,14 +75,14 @@ func ParseRange(str *string) (Range, bool) {
|
||||
return newRange(n, n), true
|
||||
}
|
||||
|
||||
func withPrefixLengths(tokens []util.Chars, begin int) []Token {
|
||||
func withPrefixLengths(tokens []string, begin int) []Token {
|
||||
ret := make([]Token, len(tokens))
|
||||
|
||||
prefixLength := begin
|
||||
for idx, token := range tokens {
|
||||
// NOTE: &tokens[idx] instead of &tokens
|
||||
ret[idx] = Token{&tokens[idx], int32(prefixLength)}
|
||||
prefixLength += token.Length()
|
||||
for idx := range tokens {
|
||||
chars := util.ToChars([]byte(tokens[idx]))
|
||||
ret[idx] = Token{&chars, int32(prefixLength)}
|
||||
prefixLength += chars.Length()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -92,16 +93,15 @@ const (
|
||||
awkWhite
|
||||
)
|
||||
|
||||
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
||||
func awkTokenizer(input string) ([]string, int) {
|
||||
// 9, 32
|
||||
ret := []util.Chars{}
|
||||
ret := []string{}
|
||||
prefixLength := 0
|
||||
state := awkNil
|
||||
numChars := input.Length()
|
||||
begin := 0
|
||||
end := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
r := input.Get(idx)
|
||||
for idx := 0; idx < len(input); idx++ {
|
||||
r := input[idx]
|
||||
white := r == 9 || r == 32
|
||||
switch state {
|
||||
case awkNil:
|
||||
@@ -119,19 +119,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
||||
if white {
|
||||
end = idx + 1
|
||||
} else {
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
ret = append(ret, input[begin:end])
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
}
|
||||
}
|
||||
if begin < end {
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
ret = append(ret, input[begin:end])
|
||||
}
|
||||
return ret, prefixLength
|
||||
}
|
||||
|
||||
// Tokenize tokenizes the given string with the delimiter
|
||||
func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
||||
func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
if delimiter.str == nil && delimiter.regex == nil {
|
||||
// AWK-style (\S+\s*)
|
||||
tokens, prefixLength := awkTokenizer(text)
|
||||
@@ -139,36 +139,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
||||
}
|
||||
|
||||
if delimiter.str != nil {
|
||||
return withPrefixLengths(text.Split(*delimiter.str), 0)
|
||||
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
|
||||
}
|
||||
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
str := text.ToString()
|
||||
for len(str) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(str)
|
||||
for len(text) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(text)
|
||||
if loc == nil {
|
||||
loc = []int{0, len(str)}
|
||||
loc = []int{0, len(text)}
|
||||
}
|
||||
last := util.Max(loc[1], 1)
|
||||
tokens = append(tokens, str[:last])
|
||||
str = str[last:]
|
||||
tokens = append(tokens, text[:last])
|
||||
text = text[last:]
|
||||
}
|
||||
}
|
||||
asRunes := make([]util.Chars, len(tokens))
|
||||
for i, token := range tokens {
|
||||
asRunes[i] = util.RunesToChars([]rune(token))
|
||||
}
|
||||
return withPrefixLengths(asRunes, 0)
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) []rune {
|
||||
ret := []rune{}
|
||||
func joinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
ret = append(ret, token.text.ToRunes()...)
|
||||
output.WriteString(token.text.ToString())
|
||||
}
|
||||
return ret
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// Transform is used to transform the input when --with-nth option is given
|
||||
@@ -181,7 +176,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.RunesToChars(joinTokens(tokens))
|
||||
chars := util.ToChars([]byte(joinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
@@ -224,15 +219,15 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
var merged util.Chars
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
merged = util.RunesToChars([]rune{})
|
||||
merged = util.ToChars([]byte{})
|
||||
case 1:
|
||||
merged = *parts[0]
|
||||
default:
|
||||
runes := []rune{}
|
||||
var output bytes.Buffer
|
||||
for _, part := range parts {
|
||||
runes = append(runes, part.ToRunes()...)
|
||||
output.WriteString(part.ToString())
|
||||
}
|
||||
merged = util.RunesToChars(runes)
|
||||
merged = util.ToChars([]byte(output.String()))
|
||||
}
|
||||
|
||||
var prefixLength int32
|
||||
|
||||
@@ -2,8 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
@@ -47,19 +45,19 @@ func TestParseRange(t *testing.T) {
|
||||
func TestTokenize(t *testing.T) {
|
||||
// AWK-style
|
||||
input := " abc: def: ghi "
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
// With delimiter
|
||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
tokens = Tokenize(input, delimiterRegexp(":"))
|
||||
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
|
||||
t.Errorf("%s", tokens)
|
||||
t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
|
||||
}
|
||||
|
||||
// With delimiter regex
|
||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
|
||||
tokens = Tokenize(input, delimiterRegexp("\\s+"))
|
||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
|
||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
|
||||
@@ -71,7 +69,7 @@ func TestTokenize(t *testing.T) {
|
||||
func TestTransform(t *testing.T) {
|
||||
input := " abc: def: ghi: jkl"
|
||||
{
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
{
|
||||
ranges := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
@@ -93,7 +91,7 @@ func TestTransform(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
tokens := Tokenize(input, delimiterRegexp(":"))
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
|
||||
@@ -182,10 +182,18 @@ func (r *LightRenderer) Init() {
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
} else {
|
||||
r.csi("J")
|
||||
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
||||
// So we do not clear the lower bottom of the screen.
|
||||
if r.clearOnExit {
|
||||
r.csi("J")
|
||||
}
|
||||
y, x := r.findOffset()
|
||||
r.mouse = r.mouse && y >= 0
|
||||
if x > 0 {
|
||||
// When --no-clear is used for repetitive relaunching, there is a small
|
||||
// time frame between fzf processes where the user keystrokes are not
|
||||
// captured by either of fzf process which can cause x offset to be
|
||||
// increased and we're left with unwanted extra new line.
|
||||
if x > 0 && r.clearOnExit {
|
||||
r.upOneLine = true
|
||||
r.makeSpace()
|
||||
}
|
||||
@@ -200,7 +208,7 @@ func (r *LightRenderer) Init() {
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
// r.csi("s")
|
||||
r.csi("s")
|
||||
if !r.fullscreen && r.mouse {
|
||||
r.yoffset, _ = r.findOffset()
|
||||
}
|
||||
@@ -411,10 +419,12 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{F12, 0, nil}
|
||||
}
|
||||
}
|
||||
// Bracketed paste mode \e[200~ / \e[201
|
||||
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
|
||||
*sz = 6
|
||||
return Event{Invalid, 0, nil}
|
||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
||||
if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
||||
// Immediately discard the sequence from the buffer and reread input
|
||||
r.buffer = r.buffer[6:]
|
||||
*sz = 0
|
||||
return r.GetChar()
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case 51:
|
||||
@@ -584,10 +594,8 @@ func (r *LightRenderer) Close() {
|
||||
}
|
||||
r.csi("J")
|
||||
}
|
||||
} else if r.fullscreen {
|
||||
r.csi("G")
|
||||
} else {
|
||||
r.move(r.height, 0)
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
@@ -697,6 +705,10 @@ func (w *LightWindow) X() int {
|
||||
return w.posx
|
||||
}
|
||||
|
||||
func (w *LightWindow) Y() int {
|
||||
return w.posy
|
||||
}
|
||||
|
||||
func (w *LightWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
@@ -831,17 +843,20 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
w.Move(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
// Wrap line
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
}
|
||||
}
|
||||
@@ -856,24 +871,25 @@ func (w *LightWindow) setBg() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy, w.posx)
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
defer w.csi("m")
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
for y := w.posy + 1; y < w.height; y++ {
|
||||
w.MoveAndClear(y, 0)
|
||||
}
|
||||
|
||||
@@ -164,6 +164,10 @@ func (w *TcellWindow) X() int {
|
||||
return w.lastX
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Y() int {
|
||||
return w.lastY
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ type Window interface {
|
||||
Close()
|
||||
|
||||
X() int
|
||||
Y() int
|
||||
Enclose(y int, x int) bool
|
||||
|
||||
Move(y int, x int)
|
||||
|
||||
@@ -3,63 +3,95 @@ package util
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
overflow64 uint64 = 0x8080808080808080
|
||||
overflow32 uint32 = 0x80808080
|
||||
)
|
||||
|
||||
type Chars struct {
|
||||
runes []rune
|
||||
bytes []byte
|
||||
slice []byte // or []rune
|
||||
inBytes bool
|
||||
trimLengthKnown bool
|
||||
trimLength uint16
|
||||
|
||||
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
|
||||
// minimize the memory footprint by not wasting padded spaces.
|
||||
Index int32
|
||||
}
|
||||
|
||||
func checkAscii(bytes []byte) (bool, int) {
|
||||
i := 0
|
||||
for ; i < len(bytes)-8; i += 8 {
|
||||
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
for ; i < len(bytes)-4; i += 4 {
|
||||
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
for ; i < len(bytes); i++ {
|
||||
if bytes[i] >= utf8.RuneSelf {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// ToChars converts byte array into rune array
|
||||
func ToChars(bytea []byte) Chars {
|
||||
var runes []rune
|
||||
ascii := true
|
||||
numBytes := len(bytea)
|
||||
for i := 0; i < numBytes; {
|
||||
if bytea[i] < utf8.RuneSelf {
|
||||
if !ascii {
|
||||
runes = append(runes, rune(bytea[i]))
|
||||
}
|
||||
i++
|
||||
} else {
|
||||
if ascii {
|
||||
ascii = false
|
||||
runes = make([]rune, i, numBytes)
|
||||
for j := 0; j < i; j++ {
|
||||
runes[j] = rune(bytea[j])
|
||||
}
|
||||
}
|
||||
r, sz := utf8.DecodeRune(bytea[i:])
|
||||
i += sz
|
||||
runes = append(runes, r)
|
||||
}
|
||||
func ToChars(bytes []byte) Chars {
|
||||
inBytes, bytesUntil := checkAscii(bytes)
|
||||
if inBytes {
|
||||
return Chars{slice: bytes, inBytes: inBytes}
|
||||
}
|
||||
if ascii {
|
||||
return Chars{bytes: bytea}
|
||||
|
||||
runes := make([]rune, bytesUntil, len(bytes))
|
||||
for i := 0; i < bytesUntil; i++ {
|
||||
runes[i] = rune(bytes[i])
|
||||
}
|
||||
return Chars{runes: runes}
|
||||
for i := bytesUntil; i < len(bytes); {
|
||||
r, sz := utf8.DecodeRune(bytes[i:])
|
||||
i += sz
|
||||
runes = append(runes, r)
|
||||
}
|
||||
return RunesToChars(runes)
|
||||
}
|
||||
|
||||
func RunesToChars(runes []rune) Chars {
|
||||
return Chars{runes: runes}
|
||||
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
}
|
||||
return *(*[]rune)(unsafe.Pointer(&chars.slice))
|
||||
}
|
||||
|
||||
func (chars *Chars) Get(i int) rune {
|
||||
if chars.runes != nil {
|
||||
return chars.runes[i]
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes[i]
|
||||
}
|
||||
return rune(chars.bytes[i])
|
||||
return rune(chars.slice[i])
|
||||
}
|
||||
|
||||
func (chars *Chars) Length() int {
|
||||
if chars.runes != nil {
|
||||
return len(chars.runes)
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return len(runes)
|
||||
}
|
||||
return len(chars.bytes)
|
||||
return len(chars.slice)
|
||||
}
|
||||
|
||||
// TrimLength returns the length after trimming leading and trailing whitespaces
|
||||
func (chars *Chars) TrimLength() int {
|
||||
func (chars *Chars) TrimLength() uint16 {
|
||||
if chars.trimLengthKnown {
|
||||
return chars.trimLength
|
||||
}
|
||||
chars.trimLengthKnown = true
|
||||
var i int
|
||||
len := chars.Length()
|
||||
for i = len - 1; i >= 0; i-- {
|
||||
@@ -80,7 +112,8 @@ func (chars *Chars) TrimLength() int {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i - j + 1
|
||||
chars.trimLength = AsUint16(i - j + 1)
|
||||
return chars.trimLength
|
||||
}
|
||||
|
||||
func (chars *Chars) TrailingWhitespaces() int {
|
||||
@@ -96,62 +129,31 @@ func (chars *Chars) TrailingWhitespaces() int {
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
if chars.runes != nil {
|
||||
return string(chars.runes)
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return string(runes)
|
||||
}
|
||||
return string(chars.bytes)
|
||||
return string(chars.slice)
|
||||
}
|
||||
|
||||
func (chars *Chars) ToRunes() []rune {
|
||||
if chars.runes != nil {
|
||||
return chars.runes
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes
|
||||
}
|
||||
runes := make([]rune, len(chars.bytes))
|
||||
for idx, b := range chars.bytes {
|
||||
bytes := chars.slice
|
||||
runes := make([]rune, len(bytes))
|
||||
for idx, b := range bytes {
|
||||
runes[idx] = rune(b)
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func (chars *Chars) Slice(b int, e int) Chars {
|
||||
if chars.runes != nil {
|
||||
return Chars{runes: chars.runes[b:e]}
|
||||
func (chars *Chars) CopyRunes(dest []rune) {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
copy(dest, runes)
|
||||
return
|
||||
}
|
||||
return Chars{bytes: chars.bytes[b:e]}
|
||||
}
|
||||
|
||||
func (chars *Chars) Split(delimiter string) []Chars {
|
||||
delim := []rune(delimiter)
|
||||
numChars := chars.Length()
|
||||
numDelim := len(delim)
|
||||
begin := 0
|
||||
ret := make([]Chars, 0, 1)
|
||||
|
||||
for index := 0; index < numChars; {
|
||||
if index+numDelim <= numChars {
|
||||
match := true
|
||||
for off, d := range delim {
|
||||
if chars.Get(index+off) != d {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// Found the delimiter
|
||||
if match {
|
||||
incr := Max(numDelim, 1)
|
||||
ret = append(ret, chars.Slice(begin, index+incr))
|
||||
index += incr
|
||||
begin = index
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Impossible to find the delimiter in the remaining substring
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
if begin < numChars || len(ret) == 0 {
|
||||
ret = append(ret, chars.Slice(begin, numChars))
|
||||
}
|
||||
return ret
|
||||
for idx, b := range chars.slice {
|
||||
dest[idx] = rune(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,27 +2,16 @@ package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestToCharsNil(t *testing.T) {
|
||||
bs := Chars{bytes: []byte{}}
|
||||
if bs.bytes == nil || bs.runes != nil {
|
||||
t.Error()
|
||||
}
|
||||
rs := RunesToChars([]rune{})
|
||||
if rs.bytes != nil || rs.runes == nil {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
if chars.ToString() != "foobar" || chars.runes != nil {
|
||||
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCharsLength(t *testing.T) {
|
||||
chars := ToChars([]byte("\tabc한글 "))
|
||||
if chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrimLength(t *testing.T) {
|
||||
check := func(str string, exp int) {
|
||||
check := func(str string, exp uint16) {
|
||||
chars := ToChars([]byte(str))
|
||||
trimmed := chars.TrimLength()
|
||||
if trimmed != exp {
|
||||
@@ -55,28 +44,3 @@ func TestTrimLength(t *testing.T) {
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
check := func(str string, delim string, tokens ...string) {
|
||||
input := ToChars([]byte(str))
|
||||
result := input.Split(delim)
|
||||
if len(result) != len(tokens) {
|
||||
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
|
||||
str, len(result), len(tokens), result)
|
||||
}
|
||||
for idx, token := range tokens {
|
||||
if result[idx].ToString() != token {
|
||||
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
|
||||
str, result[idx].ToString(), token)
|
||||
}
|
||||
}
|
||||
}
|
||||
check("abc:def::", ":", "abc:", "def:", ":")
|
||||
check("abc:def::", "-", "abc:def::")
|
||||
check("abc", "", "a", "b", "c")
|
||||
check("abc", "a", "a", "bc")
|
||||
check("abc", "ab", "ab", "c")
|
||||
check("abc", "abc", "abc")
|
||||
check("abc", "abcd", "abc")
|
||||
check("", "abcd", "")
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ Execute (fzf#shellescape with cmd.exe):
|
||||
|
||||
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
|
||||
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
|
||||
" AssertEqual '^"%%USERPROFILE%%^", fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
||||
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
||||
|
||||
Execute (Cleanup):
|
||||
unlet g:dir
|
||||
|
||||
@@ -73,6 +73,7 @@ class Tmux
|
||||
else
|
||||
raise "Unknown shell: #{shell}"
|
||||
end
|
||||
go("set-window-option -t #{@win} pane-base-index 0")
|
||||
@lines = `tput lines`.chomp.to_i
|
||||
|
||||
if shell == :fish
|
||||
@@ -259,6 +260,12 @@ class TestGoFZF < TestBase
|
||||
assert_equal 'hello', readonce.chomp
|
||||
end
|
||||
|
||||
def test_fzf_default_command_failure
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
|
||||
tmux.until { |lines| lines[-2].include?('ERROR') }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_key_bindings
|
||||
tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
|
||||
tmux.until { |lines| lines.last =~ /^>/ }
|
||||
@@ -1246,11 +1253,12 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_no_clear
|
||||
tmux.send_keys 'seq 100 | fzf --no-clear --inline-info --height 5', :Enter
|
||||
prompt = '> < 100/100'
|
||||
tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter
|
||||
prompt = '> < 10/10'
|
||||
tmux.until { |lines| lines[-1] == prompt }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' }
|
||||
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
|
||||
tmux.until { |lines| lines[-1] == prompt }
|
||||
end
|
||||
|
||||
def test_change_top
|
||||
|
||||
Reference in New Issue
Block a user