Compare commits

..

25 Commits

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

One can override the setting on FileType fzf autocmd.
2017-07-23 13:12:15 +09:00
Junegunn Choi
6dbc108da2 0.16.10 2017-07-21 18:41:11 +09:00
Junegunn Choi
bd98f988f0 Further reduce unnecessary rune array conversion
I was too quick to release 0.16.9, this commit makes --ansi processing
even faster.
2017-07-21 17:31:11 +09:00
Junegunn Choi
06301c7847 Fix regression: ANSI color in preview window not cleared 2017-07-21 16:44:59 +09:00
20 changed files with 288 additions and 76 deletions

View File

@@ -1,6 +1,16 @@
CHANGELOG CHANGELOG
========= =========
0.16.11
-------
- Performance optimization
- Fixed missing preview update
0.16.10
-------
- Fixed invalid handling of ANSI colors in preview window
- Further improved `--ansi` performance
0.16.9 0.16.9
------ ------
- Memory and performance optimization - Memory and performance optimization

105
README.md
View File

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

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jul 2017" "fzf 0.16.9" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Aug 2017" "fzf 0.16.11" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jul 2017" "fzf 0.16.9" "fzf - a command-line fuzzy finder" .TH fzf 1 "Aug 2017" "fzf 0.16.11" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -111,6 +111,9 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.B "-m, --multi" .B "-m, --multi"
Enable multi-select with tab/shift-tab Enable multi-select with tab/shift-tab
.TP .TP
.B "+m, --no-multi"
Disable multi-select
.TP
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
@@ -357,6 +360,9 @@ e.g. \fBfzf --multi | fzf --sync\fR
.B "--version" .B "--version"
Display version information and exit Display version information and exit
.TP
Note that most options have the opposite versions with \fB--no-\fR prefix.
.SH ENVIRONMENT VARIABLES .SH ENVIRONMENT VARIABLES
.TP .TP
.B FZF_DEFAULT_COMMAND .B FZF_DEFAULT_COMMAND

View File

@@ -688,7 +688,7 @@ function! s:execute_term(dict, command, temps) abort
lcd - lcd -
endif endif
endtry endtry
setlocal nospell bufhidden=wipe nobuflisted setlocal nospell bufhidden=wipe nobuflisted nonumber
setf fzf setf fzf
startinsert startinsert
return [] return []
@@ -756,7 +756,7 @@ let s:default_action = {
function! s:shortpath() function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.')) let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
let slash = (s:is_win && !&shellslash) ? '\' : '/' let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash) return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort

View File

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

View File

@@ -17,7 +17,7 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) 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 var start, end int
if pos == nil || len(*pos) == 0 { if pos == nil || len(*pos) == 0 {
start = res.Start start = res.Start

View File

@@ -55,7 +55,6 @@ func CountItems(cs []*Chunk) int {
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data []byte) bool { func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock()
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
newChunk := Chunk(make([]Item, 0, chunkSize)) newChunk := Chunk(make([]Item, 0, chunkSize))
@@ -64,17 +63,19 @@ func (cl *ChunkList) Push(data []byte) bool {
if cl.lastChunk().push(cl.trans, data, cl.count) { if cl.lastChunk().push(cl.trans, data, cl.count) {
cl.count++ cl.count++
cl.mutex.Unlock()
return true return true
} }
cl.mutex.Unlock()
return false return false
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock()
ret := make([]*Chunk, len(cl.chunks)) ret := make([]*Chunk, len(cl.chunks))
count := cl.count
copy(ret, cl.chunks) copy(ret, cl.chunks)
// Duplicate the last chunk // Duplicate the last chunk
@@ -82,5 +83,7 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
newChunk := *ret[cnt-1] newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk ret[cnt-1] = &newChunk
} }
return ret, cl.count
cl.mutex.Unlock()
return ret, count
} }

View File

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

View File

@@ -69,14 +69,14 @@ func Run(opts *Options, revision string) {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return util.RunesToChars([]rune(trimmed)), offsets return util.ToChars([]byte(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return util.RunesToChars([]rune(trimmed)), nil return util.ToChars([]byte(trimmed)), nil
} }
} }
} }
@@ -205,7 +205,6 @@ func Run(opts *Options, revision string) {
delay := true delay := true
ticks++ ticks++
eventBox.Wait(func(events *util.Events) { eventBox.Wait(func(events *util.Events) {
defer events.Clear()
for evt, value := range *events { for evt, value := range *events {
switch evt { switch evt {
@@ -265,6 +264,7 @@ func Run(opts *Options, revision string) {
} }
} }
} }
events.Clear()
}) })
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(

View File

@@ -17,7 +17,7 @@ func assert(t *testing.T, cond bool, msg ...string) {
func randResult() Result { func randResult() Result {
str := fmt.Sprintf("%d", rand.Uint32()) str := fmt.Sprintf("%d", rand.Uint32())
chars := util.RunesToChars([]rune(str)) chars := util.ToChars([]byte(str))
chars.Index = rand.Int31() chars.Index = rand.Int31()
return Result{item: &Item{text: chars}} return Result{item: &Item{text: chars}}
} }

View File

@@ -301,7 +301,12 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
} }
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
input := p.prepareInput(item) var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
if p.fuzzy { if p.fuzzy {
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
@@ -309,7 +314,12 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
} }
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
input := p.prepareInput(item) var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
offsets := []Offset{} offsets := []Offset{}
var totalScore int var totalScore int
var allPos *[]int var allPos *[]int
@@ -353,11 +363,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
return offsets, totalScore, allPos return offsets, totalScore, allPos
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) transformInput(item *Item) []Token {
if len(p.nth) == 0 {
return []Token{Token{text: &item.text, prefixLength: 0}}
}
if item.transformed != nil { if item.transformed != nil {
return *item.transformed return *item.transformed
} }

View File

@@ -78,7 +78,7 @@ func TestExact(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
res, pos := algo.ExactMatchNaive( 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 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) 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) { match := func(str string, sidxExpected int, eidxExpected int) {
res, pos := algo.EqualMatch( 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 { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
@@ -140,7 +140,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
for _, extended := range []bool{false, true} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{
Item{ Item{
text: util.RunesToChars([]rune("junegunn")), text: util.ToChars([]byte("junegunn")),
origText: &origBytes, origText: &origBytes,
transformed: &trans}, transformed: &trans},
} }

View File

@@ -101,6 +101,7 @@ type Terminal struct {
printer func(string) printer func(string)
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
version int64
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts preview previewOpts
previewer previewer previewer previewer
@@ -712,7 +713,7 @@ func (t *Terminal) printHeader() {
trimmed, colors, newState := extractColor(lineStr, state, nil) trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState state = newState
item := &Item{ item := &Item{
text: util.RunesToChars([]rune(trimmed)), text: util.ToChars([]byte(trimmed)),
colors: colors} colors: colors}
t.move(line, 2, true) t.move(line, 2, true)
@@ -1173,8 +1174,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
} }
for idx, item := range items { for idx, item := range items {
chars := util.RunesToChars([]rune(item.AsString(stripAnsi))) tokens := Tokenize(item.AsString(stripAnsi), delimiter)
tokens := Tokenize(chars.ToString(), delimiter)
trans := Transform(tokens, ranges) trans := Transform(tokens, ranges)
str := string(joinTokens(trans)) str := string(joinTokens(trans))
if delimiter.str != nil { if delimiter.str != nil {
@@ -1258,6 +1258,24 @@ func (t *Terminal) truncateQuery() {
t.cx = util.Constrain(t.cx, 0, len(t.input)) t.cx = util.Constrain(t.cx, 0, len(t.input))
} }
func (t *Terminal) selectItem(item *Item) {
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) toggleItem(item *Item) {
if _, found := t.selected[item.Index()]; !found {
t.selectItem(item)
} else {
t.deselectItem(item)
}
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
// prof := profile.Start(profile.ProfilePath("/tmp/")) // prof := profile.Start(profile.ProfilePath("/tmp/"))
@@ -1360,6 +1378,7 @@ func (t *Terminal) Loop() {
go func() { go func() {
var focused *Item var focused *Item
var version int64
for { for {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
@@ -1376,7 +1395,8 @@ func (t *Terminal) Loop() {
case reqList: case reqList:
t.printList() t.printList()
currentFocus := t.currentItem() currentFocus := t.currentItem()
if currentFocus != focused { if currentFocus != focused || version != t.version {
version = t.version
focused = currentFocus focused = currentFocus
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false) _, list := t.buildPlusList(t.preview.command, false)
@@ -1442,22 +1462,9 @@ func (t *Terminal) Loop() {
} }
} }
} }
selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y).item
if !selectItem(item) {
delete(t.selected, item.Index())
}
}
toggle := func() { toggle := func() {
if t.cy < t.merger.Length() { if t.cy < t.merger.Length() {
toggleY(t.cy) t.toggleItem(t.merger.Get(t.cy).item)
req(reqInfo) req(reqInfo)
} }
} }
@@ -1571,17 +1578,14 @@ func (t *Terminal) Loop() {
case actSelectAll: case actSelectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i).item t.selectItem(t.merger.Get(i).item)
selectItem(item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }
case actDeselectAll: case actDeselectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { t.selected = make(map[int32]selectedItem)
item := t.merger.Get(i) t.version++
delete(t.selected, item.Index())
}
req(reqList, reqInfo) req(reqList, reqInfo)
} }
case actToggle: case actToggle:
@@ -1592,7 +1596,7 @@ func (t *Terminal) Loop() {
case actToggleAll: case actToggleAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
toggleY(i) t.toggleItem(t.merger.Get(i).item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }

View File

@@ -10,7 +10,7 @@ import (
func newItem(str string) *Item { func newItem(str string) *Item {
bytes := []byte(str) bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil) 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) { func TestReplacePlaceholder(t *testing.T) {

View File

@@ -32,7 +32,8 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R"
func openTtyIn() *os.File { func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
panic("Failed to open " + consoleDevice) fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
} }
return in return in
} }
@@ -882,8 +883,8 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
bg = w.bg bg = w.bg
} }
if w.csiColor(fg, bg, attr) { if w.csiColor(fg, bg, attr) {
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
defer w.csi("m") defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
} }
return w.fill(text, w.setBg) return w.fill(text, w.setBg)
} }

View File

@@ -24,12 +24,12 @@ type Chars struct {
func checkAscii(bytes []byte) (bool, int) { func checkAscii(bytes []byte) (bool, int) {
i := 0 i := 0
for ; i < len(bytes)-8; i += 8 { for ; i <= len(bytes)-8; i += 8 {
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 { if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i return false, i
} }
} }
for ; i < len(bytes)-4; i += 4 { for ; i <= len(bytes)-4; i += 4 {
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 { if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i return false, i
} }
@@ -65,6 +65,14 @@ func RunesToChars(runes []rune) Chars {
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false} return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
} }
func (chars *Chars) IsBytes() bool {
return chars.inBytes
}
func (chars *Chars) Bytes() []byte {
return chars.slice
}
func (chars *Chars) optionalRunes() []rune { func (chars *Chars) optionalRunes() []rune {
if chars.inBytes { if chars.inBytes {
return nil return nil

View File

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

View File

@@ -1274,6 +1274,16 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-3] == '> 11' } tmux.until { |lines| lines[-3] == '> 11' }
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_preview_update_on_select
tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter)
tmux.until { |lines| lines.item_count == 10 }
tmux.send_keys 'a'
tmux.until { |lines| lines.any? { |line| line.include? '1 2 3 4 5' } }
tmux.send_keys 'a'
tmux.until { |lines| !lines.any? { |line| line.include? '1 2 3 4 5' } }
end
end end
module TestShell module TestShell