diff --git a/.travis.yml b/.travis.yml index 692ade7ae7e5d3c13423acc4a8e98c925ccbc054..69086778e3f2e9e69d8b6b09631fc1e7ca16cd3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,15 @@ language: ruby +rvm: +- 2.2.0 install: - sudo apt-get update - sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo add-apt-repository -y ppa:pi-rho/dev +- sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-get update - sudo apt-get install -y tmux=1.9a-1~ppa1~p +- sudo apt-get install -y zsh fish script: | export GOROOT=~/go1.4 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..b9e8e77329ad21bc1a42c0e7ae5f41265fa022c4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +CHANGELOG +========= + +0.9.4 +----- + +### New features + +#### Added `--tac` option to reverse the order of the input. + +One might argue that this option is unnecessary since we can already put `tac` +or `tail -r` in the command pipeline to achieve the same result. However, the +advantage of `--tac` is that it does not block until the input is complete. + +### *Backward incompatible changes* + +#### Changed behavior on `--no-sort` + +`--no-sort` option will no longer reverse the display order within finder. You +may want to use the new `--tac` option with `--no-sort`. + +``` +history | fzf +s --tac +``` + +### Improvements + +#### `--filter` will not block when sort is disabled + +When fzf works in filtering mode (`--filter`) and sort is disabled +(`--no-sort`), there's no need to block until input is complete. The new +version of fzf will print the matches on-the-fly when the following condition +is met: + + --filter TERM --no-sort [--no-tac --no-sync] + +or simply: + + -f TERM +s + +This change removes unnecessary delay in the use cases like the following: + + fzf -f xxx +s | head -5 + +However, in this case, fzf processes the lines sequentially, so it cannot +utilize multiple cores, and fzf will run slightly slower than the previous +mode of execution where filtering is done in parallel after the entire input +is loaded. If the user is concerned about this performance problem, one can +add `--sync` option to re-enable buffering. + +0.9.3 +----- + +### New features +- Added `--sync` option for multi-staged filtering + +### Improvements +- `--select-1` and `--exit-0` will start finder immediately when the condition + cannot be met + diff --git a/README.md b/README.md index 16cedbfa2563faf9708650a2ebcc919fdb6c6d3d..e5ada36b39400a337a82c782abc5109164092987 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Usage ``` usage: fzf [options] - Search + Search mode -x, --extended Extended-search mode -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) @@ -87,8 +87,9 @@ usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort Sort the result - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + (e.g. 'history | fzf --tac --no-sort') Interface -m, --multi Enable multi-select with tab/shift-tab @@ -128,13 +129,6 @@ files excluding hidden ones. (You can override the default command with vim $(fzf) ``` -If you want to preserve the exact sequence of the input, provide `--no-sort` (or -`+s`) option. - -```sh -history | fzf +s -``` - ### Keys Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press @@ -197,7 +191,7 @@ fd() { # fh - repeat history fh() { - eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//') + eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s --tac | sed 's/ *[0-9]* *//') } # fkill - kill process diff --git a/install b/install index 5cd067285e95592887fd14a878e49c0112ab4ff9..8b53f721898071d4e6223b5d8d0a09026c614318 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.3 +version=0.9.4 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -245,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' @@ -263,7 +263,7 @@ else bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory @@ -323,7 +323,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//") + LBUFFER=$(fc -l 1 | fzf +s --tac +m -n2..,.. | sed "s/ *[0-9*]* *//") zle redisplay } zle -N fzf-history-widget @@ -412,7 +412,7 @@ function fzf_key_bindings end function __fzf_ctrl_r - history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result + history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index e37a8b221b2fab333993945fcf995cef91cff4d9..b5fd7c0877f0dfa2f85a5174beacf3a4be1afc58 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index bbe065e648ba7fe6f727ee826997e4b89a0512d8..c03f43a299f78c618c4b1d6dc191a403427c290f 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index 9d28b322745d41ba45c608cbf588724386f1e4f6..4778a6d11c3673698ee1d400e057511f382f9eab 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \ # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/constants.go b/src/constants.go index 7d542234f3b3e645f1352a4fe7e14b6ab18df1c1..f5138534c4788e404bbaaf5af66c9a7382f21991 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.3" +const Version = "0.9.4" // fzf events const ( diff --git a/src/core.go b/src/core.go index ea97b4e6785ce1e1e5186f2dc878befd8aec3240..62190d084a63efb24ff8f4ae685c9b3fb551d7c5 100644 --- a/src/core.go +++ b/src/core.go @@ -85,33 +85,47 @@ func Run(options *Options) { } // Reader - reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} - go reader.ReadSource() + streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync + if !streamingFilter { + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + go reader.ReadSource() + } // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) } - matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) + matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { - pattern := patternBuilder([]rune(*opts.Filter)) - - eventBox.Unwatch(EvtReadNew) - eventBox.WaitFor(EvtReadFin) - - snapshot, _ := chunkList.Snapshot() - merger, _ := matcher.scan(MatchRequest{ - chunks: snapshot, - pattern: pattern}) - if opts.PrintQuery { fmt.Println(*opts.Filter) } - for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).AsString()) + + pattern := patternBuilder([]rune(*opts.Filter)) + + if streamingFilter { + reader := Reader{ + func(str string) { + item := chunkList.trans(&str, 0) + if pattern.MatchItem(item) { + fmt.Println(*item.text) + } + }, eventBox} + reader.ReadSource() + } else { + eventBox.Unwatch(EvtReadNew) + eventBox.WaitFor(EvtReadFin) + + snapshot, _ := chunkList.Snapshot() + merger, _ := matcher.scan(MatchRequest{ + chunks: snapshot, + pattern: pattern}) + for i := 0; i < merger.Length(); i++ { + fmt.Println(merger.Get(i).AsString()) + } } os.Exit(0) } diff --git a/src/item.go b/src/item.go index 4cbd3f987c51c9a3c0757445e0d008f590502d0a..2b8a9d134488fea5077e5c9fb948b99d88c2b459 100644 --- a/src/item.go +++ b/src/item.go @@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool { irank := a[i].Rank(true) jrank := a[j].Rank(true) - return compareRanks(irank, jrank) + return compareRanks(irank, jrank, false) } -func compareRanks(irank Rank, jrank Rank) bool { +// ByRelevanceTac is for sorting Items +type ByRelevanceTac []*Item + +func (a ByRelevanceTac) Len() int { + return len(a) +} + +func (a ByRelevanceTac) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevanceTac) Less(i, j int) bool { + irank := a[i].Rank(true) + jrank := a[j].Rank(true) + + return compareRanks(irank, jrank, true) +} + +func compareRanks(irank Rank, jrank Rank, tac bool) bool { if irank.matchlen < jrank.matchlen { return true } else if irank.matchlen > jrank.matchlen { @@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool { return false } - if irank.index <= jrank.index { - return true - } - return false + return (irank.index <= jrank.index) != tac } diff --git a/src/item_test.go b/src/item_test.go index 0e83631a17e477fd3f7df52568fec81c705b9cb6..372ab4aeecd949f4ca7b75d84c22391212f1b005 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -20,12 +20,19 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) || + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { t.Error("Invalid order") } + + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) || + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + t.Error("Invalid order (tac)") + } } // Match length, string length, index diff --git a/src/matcher.go b/src/matcher.go index bfe9d28704be06eb9674c0d06d45c20b0aaf6496..0879a0885da4d7060bd5ac0d457eb401fdaab157 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -21,6 +21,7 @@ type MatchRequest struct { type Matcher struct { patternBuilder func([]rune) *Pattern sort bool + tac bool eventBox *util.EventBox reqBox *util.EventBox partitions int @@ -38,10 +39,11 @@ const ( // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, - sort bool, eventBox *util.EventBox) *Matcher { + sort bool, tac bool, eventBox *util.EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, + tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), partitions: runtime.NumCPU(), @@ -159,7 +161,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { countChan <- len(matches) } if !empty && m.sort { - sort.Sort(ByRelevance(sliceMatches)) + if m.tac { + sort.Sort(ByRelevanceTac(sliceMatches)) + } else { + sort.Sort(ByRelevance(sliceMatches)) + } } resultChan <- partialResult{idx, sliceMatches} }(idx, chunks) @@ -195,7 +201,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - return NewMerger(partialResults, !empty && m.sort), false + return NewMerger(partialResults, !empty && m.sort, m.tac), false } // Reset is called to interrupt/signal the ongoing search diff --git a/src/merger.go b/src/merger.go index 5bfc81d5ccf93c3e890cf34cb74ab7e64ef35f24..41323c18d3b1c74dcb8f012bed20a26d285c32a3 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,7 +3,7 @@ package fzf import "fmt" // Merger with no data -var EmptyMerger = NewMerger([][]*Item{}, false) +var EmptyMerger = NewMerger([][]*Item{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list @@ -12,17 +12,19 @@ type Merger struct { merged []*Item cursors []int sorted bool + tac bool final bool count int } // NewMerger returns a new Merger -func NewMerger(lists [][]*Item, sorted bool) *Merger { +func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { mg := Merger{ lists: lists, merged: []*Item{}, cursors: make([]int, len(lists)), sorted: sorted, + tac: tac, final: false, count: 0} @@ -39,19 +41,21 @@ func (mg *Merger) Length() int { // Get returns the pointer to the Item object indexed by the given integer func (mg *Merger) Get(idx int) *Item { - if len(mg.lists) == 1 { - return mg.lists[0][idx] - } else if !mg.sorted { - for _, list := range mg.lists { - numItems := len(list) - if idx < numItems { - return list[idx] - } - idx -= numItems + if mg.sorted { + return mg.mergedGet(idx) + } + + if mg.tac { + idx = mg.Length() - idx - 1 + } + for _, list := range mg.lists { + numItems := len(list) + if idx < numItems { + return list[idx] } - panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) + idx -= numItems } - return mg.mergedGet(idx) + panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } func (mg *Merger) mergedGet(idx int) *Item { @@ -66,7 +70,7 @@ func (mg *Merger) mergedGet(idx int) *Item { } if cursor >= 0 { rank := list[cursor].Rank(false) - if minIdx < 0 || compareRanks(rank, minRank) { + if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx } diff --git a/src/merger_test.go b/src/merger_test.go index f79da09a652b2ff9594c0fae9c6157bbe9b15abf..b69d63386628c648ed61e2ae4f55422e51ae7e0a 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) { cnt := len(items) // Not sorted: same order - mg := NewMerger(lists, false) + mg := NewMerger(lists, false, false) assert(t, cnt == mg.Length(), "Invalid Length") for i := 0; i < cnt; i++ { assert(t, items[i] == mg.Get(i), "Invalid Get") @@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) { cnt := len(items) // Sorted sorted order - mg := NewMerger(lists, true) + mg := NewMerger(lists, true, false) assert(t, cnt == mg.Length(), "Invalid Length") sort.Sort(ByRelevance(items)) for i := 0; i < cnt; i++ { @@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) { } // Inverse order - mg2 := NewMerger(lists, true) + mg2 := NewMerger(lists, true, false) for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) diff --git a/src/options.go b/src/options.go index c426e7774218705a332054b08f9eba3a2f9ae177..dc8f0b84c1052f62ff81d2590b33284a666b027e 100644 --- a/src/options.go +++ b/src/options.go @@ -11,7 +11,7 @@ import ( const usage = `usage: fzf [options] - Search + Search mode -x, --extended Extended-search mode -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) @@ -23,8 +23,9 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort Sort the result - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + (e.g. 'history | fzf --tac --no-sort') Interface -m, --multi Enable multi-select with tab/shift-tab @@ -78,6 +79,7 @@ type Options struct { WithNth []Range Delimiter *regexp.Regexp Sort int + Tac bool Multi bool Mouse bool Color bool @@ -102,6 +104,7 @@ func defaultOptions() *Options { WithNth: make([]Range, 0), Delimiter: nil, Sort: 1000, + Tac: false, Multi: false, Mouse: true, Color: true, @@ -212,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sort = optionalNumeric(allArgs, &i) case "+s", "--no-sort": opts.Sort = 0 + case "--tac": + opts.Tac = true + case "--no-tac": + opts.Tac = false case "-i": opts.Case = CaseIgnore case "+i": diff --git a/src/pattern.go b/src/pattern.go index 17e3b6b88ca5c6af5285412582ed5309f4950242..725ce2db6029aafe88ba10b6d09dcd5409686c29 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -219,12 +219,7 @@ Loop: } } - var matches []*Item - if p.mode == ModeFuzzy { - matches = p.fuzzyMatch(space) - } else { - matches = p.extendedMatch(space) - } + matches := p.matchChunk(space) if !p.hasInvTerm { _cache.Add(chunk, cacheKey, matches) @@ -232,6 +227,35 @@ Loop: return matches } +func (p *Pattern) matchChunk(chunk *Chunk) []*Item { + matches := []*Item{} + if p.mode == ModeFuzzy { + for _, item := range *chunk { + if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 { + matches = append(matches, + dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) + } + } + } else { + for _, item := range *chunk { + if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) { + matches = append(matches, dupItem(item, offsets)) + } + } + } + return matches +} + +// MatchItem returns true if the Item is a match +func (p *Pattern) MatchItem(item *Item) bool { + if p.mode == ModeFuzzy { + sidx, _ := p.fuzzyMatch(item) + return sidx >= 0 + } + offsets := p.extendedMatch(item) + return len(offsets) == len(p.terms) +} + func dupItem(item *Item, offsets []Offset) *Item { sort.Sort(ByOrder(offsets)) return &Item{ @@ -243,39 +267,26 @@ func dupItem(item *Item, offsets []Offset) *Item { rank: Rank{0, 0, item.index}} } -func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { - matches := []*Item{} - for _, item := range *chunk { - input := p.prepareInput(item) - if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 { - matches = append(matches, - dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) - } - } - return matches +func (p *Pattern) fuzzyMatch(item *Item) (int, int) { + input := p.prepareInput(item) + return p.iter(algo.FuzzyMatch, input, p.text) } -func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { - matches := []*Item{} - for _, item := range *chunk { - input := p.prepareInput(item) - offsets := []Offset{} - for _, term := range p.terms { - pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { - if term.inv { - break - } - offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) - } else if term.inv { - offsets = append(offsets, Offset{0, 0}) +func (p *Pattern) extendedMatch(item *Item) []Offset { + input := p.prepareInput(item) + offsets := []Offset{} + for _, term := range p.terms { + pfun := p.procFun[term.typ] + if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { + if term.inv { + break } - } - if len(offsets) == len(p.terms) { - matches = append(matches, dupItem(item, offsets)) + offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) + } else if term.inv { + offsets = append(offsets, Offset{0, 0}) } } - return matches + return offsets } func (p *Pattern) prepareInput(item *Item) *Transformed { diff --git a/src/pattern_test.go b/src/pattern_test.go index 4d36eda53e562bb803d2720025d81e9ee6e4031c..67542f2144654fd5e71ad3e83c2a93698ad90a66 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) { tokens := Tokenize(strptr("junegunn"), nil) trans := Transform(tokens, []Range{Range{1, 1}}) - for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { + for _, mode := range []Mode{ModeFuzzy, ModeExtended} { chunk := Chunk{ &Item{ text: strptr("junegunn"), origText: strptr("junegunn.choi"), transformed: trans}, } - matches := fun(&chunk) + pattern.mode = mode + matches := pattern.matchChunk(&chunk) if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].transformed != trans { diff --git a/src/terminal.go b/src/terminal.go index 3d914ac5e055d0aa644a7862be86741920c4aa01..bd426d1a15ed017505bf6313a44bf50e2cadd31a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -22,7 +22,6 @@ import ( type Terminal struct { prompt string reverse bool - tac bool cx int cy int offset int @@ -85,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ prompt: opts.Prompt, - tac: opts.Sort == 0, reverse: opts.Reverse, cx: len(input), cy: 0, @@ -148,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) { t.reqBox.Set(reqList, nil) } -func (t *Terminal) listIndex(y int) int { - if t.tac { - return t.merger.Length() - y - 1 - } - return y -} - func (t *Terminal) output() { if t.printQuery { fmt.Println(string(t.input)) @@ -162,7 +153,7 @@ func (t *Terminal) output() { if len(t.selected) == 0 { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { - fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) + fmt.Println(t.merger.Get(t.cy).AsString()) } } else { sels := make([]selectedItem, 0, len(t.selected)) @@ -246,7 +237,7 @@ func (t *Terminal) printList() { for i := 0; i < maxy; i++ { t.move(i+2, 0, true) if i < count { - t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) } } } @@ -525,9 +516,8 @@ func (t *Terminal) Loop() { } } toggle := func() { - idx := t.listIndex(t.cy) - if idx < t.merger.Length() { - item := t.merger.Get(idx) + if t.cy < t.merger.Length() { + item := t.merger.Get(t.cy) if _, found := t.selected[item.text]; !found { var strptr *string if item.origText != nil { @@ -650,7 +640,7 @@ func (t *Terminal) Loop() { } else if me.Double { // Double-click if my >= 2 { - if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { + if t.vset(my-2) && t.cy < t.merger.Length() { req(reqClose) } } diff --git a/test/test_go.rb b/test/test_go.rb index fe32a4ea9a07392970117bc69bb42706fce3d227..6aa438b09c99ca66c4021039a9dc6254c277f51b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2,11 +2,20 @@ # encoding: utf-8 require 'minitest/autorun' +require 'fileutils' class NilClass def include? str false end + + def start_with? str + false + end + + def end_with? str + false + end end module Temp @@ -15,7 +24,7 @@ module Temp waited = 0 while waited < 5 begin - data = File.read(name) + data = `cat #{name}` return data unless data.empty? rescue sleep 0.1 @@ -30,6 +39,20 @@ module Temp end end +class Shell + class << self + def bash + 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' + end + + def zsh + FileUtils.mkdir_p '/tmp/fzf-zsh' + FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' + 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' + end + end +end + class Tmux include Temp @@ -37,18 +60,33 @@ class Tmux attr_reader :win - def initialize shell = 'bash' - @win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first + def initialize shell = :bash + @win = + case shell + when :bash + go("new-window -d -P -F '#I' '#{Shell.bash}'").first + when :zsh + go("new-window -d -P -F '#I' '#{Shell.zsh}'").first + when :fish + go("new-window -d -P -F '#I' 'fish'").first + else + raise "Unknown shell: #{shell}" + end @lines = `tput lines`.chomp.to_i + + if shell == :fish + send_keys('function fish_prompt; end; clear', :Enter) + self.until { |lines| lines.empty? } + end end def closed? !go("list-window -F '#I'").include?(win) end - def close timeout = 1 + def close send_keys 'C-c', 'C-u', 'exit', :Enter - wait(timeout) { closed? } + wait { closed? } end def kill @@ -56,35 +94,68 @@ class Tmux end def send_keys *args + target = + if args.last.is_a?(Hash) + hash = args.pop + go("select-window -t #{win}") + "#{win}.#{hash[:pane]}" + else + win + end args = args.map { |a| %{"#{a}"} }.join ' ' - go("send-keys -t #{win} #{args}") + go("send-keys -t #{target} #{args}") end - def capture - go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") - raise "Window not found" if $?.exitstatus != 0 + def capture opts = {} + timeout, pane = defaults(opts).values_at(:timeout, :pane) + waited = 0 + loop do + go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}") + break if $?.exitstatus == 0 + + if waited > timeout + raise "Window not found" + end + waited += 0.1 + sleep 0.1 + end readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse end - def until timeout = 1 - wait(timeout) { yield capture } + def until opts = {} + lines = nil + wait(opts) do + yield lines = capture(opts) + end + lines end + def prepare + self.send_keys 'echo hello', :Enter + self.until { |lines| lines[-1].start_with?('hello') } + self.send_keys 'clear', :Enter + self.until { |lines| lines.empty? } + end private - def wait timeout = 1 + def defaults opts + { timeout: 5, pane: 0 }.merge(opts) + end + + def wait opts = {} + timeout, pane = defaults(opts).values_at(:timeout, :pane) waited = 0 until yield - waited += 0.1 - sleep 0.1 if waited > timeout hl = '=' * 10 puts hl - capture.each_with_index do |line, idx| + capture(opts).each_with_index do |line, idx| puts [idx.to_s.rjust(2), line].join(': ') end puts hl raise "timeout" end + waited += 0.1 + sleep 0.1 end end @@ -93,7 +164,7 @@ private end end -class TestGoFZF < MiniTest::Unit::TestCase +class TestBase < Minitest::Test include Temp FIN = 'FIN' @@ -104,11 +175,6 @@ class TestGoFZF < MiniTest::Unit::TestCase def setup ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_COMMAND' - @tmux = Tmux.new - end - - def teardown - @tmux.kill end def fzf(*opts) @@ -129,10 +195,22 @@ class TestGoFZF < MiniTest::Unit::TestCase }.compact "fzf #{opts.join ' '}" end +end + +class TestGoFZF < TestBase + def setup + super + @tmux = Tmux.new + end + + def teardown + @tmux.kill + end def test_vanilla tmux.send_keys "seq 1 100000 | #{fzf}", :Enter - tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } + tmux.until(timeout: 10) { |lines| + lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] assert_equal '> 1', lines[-3] @@ -322,5 +400,166 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys 'C-K', :Enter assert_equal ['1919'], readonce.split($/) end + + def test_tac + tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[1000 999 998], readonce.split($/) + end + + def test_tac_sort + tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys '99' + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[99 999 998], readonce.split($/) + end + + def test_tac_nosort + tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys '00' + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[1000 900 800], readonce.split($/) + end +end + +module TestShell + def setup + super + end + + def teardown + @tmux.kill + end + + def test_ctrl_t + tmux.prepare + tmux.send_keys 'C-t', pane: 0 + lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') + tmux.send_keys :BTab, :BTab, :Enter, pane: 1 + tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.send_keys 'C-c' + + # FZF_TMUX=0 + new_shell + tmux.send_keys 'C-t', pane: 0 + lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' } + expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') + tmux.send_keys :BTab, :BTab, :Enter, pane: 0 + tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.send_keys 'C-c', 'C-d' + end + + def test_alt_c + tmux.prepare + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines[-1].start_with? '>' } + expected = lines[-3][2..-1] + p expected + tmux.send_keys :Enter + tmux.prepare + tmux.send_keys :pwd, :Enter + tmux.until { |lines| p lines; lines[-1].end_with?(expected) } + end + + def test_ctrl_r + tmux.prepare + tmux.send_keys 'echo 1st', :Enter; tmux.prepare + tmux.send_keys 'echo 2nd', :Enter; tmux.prepare + tmux.send_keys 'echo 3d', :Enter; tmux.prepare + tmux.send_keys 'echo 3rd', :Enter; tmux.prepare + tmux.send_keys 'echo 4th', :Enter; tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys '3d' + tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'echo 3rd' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == '3rd' } + end +end + +class TestBash < TestBase + include TestShell + + def new_shell + tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter + tmux.prepare + end + + def setup + super + @tmux = Tmux.new :bash + end + + def test_file_completion + tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter + tmux.prepare + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys :BTab, :BTab, :Enter + tmux.until { |lines| + lines[-1].include?('/tmp/fzf-test/10') && + lines[-1].include?('/tmp/fzf-test/100') + } + end + + def test_dir_completion + tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}', :Enter + tmux.prepare + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys :BTab, :BTab # BTab does not work here + tmux.send_keys 55 + tmux.until { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' } + end + + def test_process_completion + tmux.send_keys 'sleep 12345 &', :Enter + lines = tmux.until { |lines| lines[-1].start_with? '[1]' } + pid = lines[-1].split.last + tmux.prepare + tmux.send_keys 'kill ', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys 'sleep12345' + tmux.until { |lines| lines[-3].include? 'sleep 12345' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == "kill #{pid}" } + end +end + +class TestZsh < TestBase + include TestShell + + def new_shell + tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter + tmux.prepare + end + + def setup + super + @tmux = Tmux.new :zsh + end +end + +class TestFish < TestBase + include TestShell + + def new_shell + tmux.send_keys 'env FZF_TMUX=0 fish', :Enter + tmux.send_keys 'function fish_prompt; end; clear', :Enter + tmux.until { |lines| lines.empty? } + end + + def setup + super + @tmux = Tmux.new :fish + end end diff --git a/test/test_ruby.rb b/test/test_ruby.rb index 674ed3be265bce916d3ced3001fd23cf811d86f2..25f923b1211f378459e3a3b13afa1a04b1e48e85 100644 --- a/test/test_ruby.rb +++ b/test/test_ruby.rb @@ -54,7 +54,7 @@ class MockTTY end end -class TestRubyFZF < MiniTest::Unit::TestCase +class TestRubyFZF < Minitest::Test def setup ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_OPTS'