summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md32
-rw-r--r--README.md16
-rwxr-xr-xinstall10
-rw-r--r--src/constants.go2
-rw-r--r--src/core.go2
-rw-r--r--src/item.go27
-rw-r--r--src/item_test.go15
-rw-r--r--src/matcher.go12
-rw-r--r--src/merger.go32
-rw-r--r--src/merger_test.go6
-rw-r--r--src/options.go13
-rw-r--r--src/terminal.go20
-rw-r--r--test/test_go.rb27
-rw-r--r--test/test_ruby.rb2
14 files changed, 147 insertions, 69 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..914a5822
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,32 @@
+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*
+
+- `--no-sort` option will no longer reverse the display order. You may want to
+ use the new `--tac` option with `--no-sort`.
+```
+history | fzf +s --tac
+```
+
+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 16cedbfa..e5ada36b 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 5cd06728..8b53f721 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/constants.go b/src/constants.go
index 7d542234..f5138534 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 ea97b4e6..ec4c5e8d 100644
--- a/src/core.go
+++ b/src/core.go
@@ -93,7 +93,7 @@ func Run(options *Options) {
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 {
diff --git a/src/item.go b/src/item.go
index 4cbd3f98..2b8a9d13 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 0e83631a..372ab4ae 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 bfe9d287..0879a088 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 5bfc81d5..41323c18 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 f79da09a..b69d6338 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 c426e777..dc8f0b84 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/terminal.go b/src/terminal.go
index 3d914ac5..bd426d1a 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 fe32a4ea..889ace49 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -15,7 +15,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
@@ -93,7 +93,7 @@ private
end
end
-class TestGoFZF < MiniTest::Unit::TestCase
+class TestGoFZF < Minitest::Test
include Temp
FIN = 'FIN'
@@ -322,5 +322,28 @@ 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
diff --git a/test/test_ruby.rb b/test/test_ruby.rb
index 674ed3be..25f923b1 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'