Add --tac option and reverse display order of --no-sort

DISCLAIMER: This is a backward incompatible change
This commit is contained in:
Junegunn Choi 2015-02-26 01:42:15 +09:00
parent 4a1752d3fc
commit c1aa5c5f33
14 changed files with 148 additions and 70 deletions

32
CHANGELOG.md Normal file
View File

@ -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

View File

@ -75,7 +75,7 @@ Usage
``` ```
usage: fzf [options] usage: fzf [options]
Search Search mode
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case 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) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
-s, --sort Sort the result +s, --no-sort Do not sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -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) vim $(fzf)
``` ```
If you want to preserve the exact sequence of the input, provide `--no-sort` (or
`+s`) option.
```sh
history | fzf +s
```
### Keys ### Keys
Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press 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 - repeat history
fh() { 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 # fkill - kill process

10
install
View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.3 version=0.9.4
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@ -245,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
fi fi
# CTRL-R - Paste the selected command from history into the command line # 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 # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' 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"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # 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"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # 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 # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { 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 redisplay
} }
zle -N fzf-history-widget zle -N fzf-history-widget
@ -412,7 +412,7 @@ function fzf_key_bindings
end end
function __fzf_ctrl_r 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) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@ -5,7 +5,7 @@ import (
) )
// Current version // Current version
const Version = "0.9.3" const Version = "0.9.4"
// fzf events // fzf events
const ( const (

View File

@ -93,7 +93,7 @@ func Run(options *Options) {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) 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 // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {

View File

@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true) irank := a[i].Rank(true)
jrank := a[j].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 { if irank.matchlen < jrank.matchlen {
return true return true
} else if irank.matchlen > jrank.matchlen { } else if irank.matchlen > jrank.matchlen {
@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool {
return false return false
} }
if irank.index <= jrank.index { return (irank.index <= jrank.index) != tac
return true
}
return false
} }

View File

@ -20,12 +20,19 @@ func TestOffsetSort(t *testing.T) {
} }
func TestRankComparison(t *testing.T) { func TestRankComparison(t *testing.T) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order") 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 // Match length, string length, index

View File

@ -21,6 +21,7 @@ type MatchRequest struct {
type Matcher struct { type Matcher struct {
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool
eventBox *util.EventBox eventBox *util.EventBox
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
@ -38,10 +39,11 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: runtime.NumCPU(),
@ -159,8 +161,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
countChan <- len(matches) countChan <- len(matches)
} }
if !empty && m.sort { if !empty && m.sort {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches)) sort.Sort(ByRelevance(sliceMatches))
} }
}
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks) }(idx, chunks)
} }
@ -195,7 +201,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches 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 // Reset is called to interrupt/signal the ongoing search

View File

@ -3,7 +3,7 @@ package fzf
import "fmt" import "fmt"
// Merger with no data // 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 // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
@ -12,17 +12,19 @@ type Merger struct {
merged []*Item merged []*Item
cursors []int cursors []int
sorted bool sorted bool
tac bool
final bool final bool
count int count int
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool) *Merger { func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
lists: lists, lists: lists,
merged: []*Item{}, merged: []*Item{},
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
tac: tac,
final: false, final: false,
count: 0} count: 0}
@ -39,9 +41,13 @@ func (mg *Merger) Length() int {
// Get returns the pointer to the Item object indexed by the given integer // Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item { func (mg *Merger) Get(idx int) *Item {
if len(mg.lists) == 1 { if mg.sorted {
return mg.lists[0][idx] return mg.mergedGet(idx)
} else if !mg.sorted { }
if mg.tac {
idx = mg.Length() - idx - 1
}
for _, list := range mg.lists { for _, list := range mg.lists {
numItems := len(list) numItems := len(list)
if idx < numItems { if idx < numItems {
@ -50,8 +56,6 @@ func (mg *Merger) Get(idx int) *Item {
idx -= numItems idx -= numItems
} }
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
}
return mg.mergedGet(idx)
} }
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Item {
@ -66,7 +70,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].Rank(false) rank := list[cursor].Rank(false)
if minIdx < 0 || compareRanks(rank, minRank) { if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank minRank = rank
minIdx = listIdx minIdx = listIdx
} }

View File

@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(lists, false) mg := NewMerger(lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(lists, true) mg := NewMerger(lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(lists, true) mg2 := NewMerger(lists, true, false)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@ -11,7 +11,7 @@ import (
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
Search Search mode
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case 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) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
-s, --sort Sort the result +s, --no-sort Do not sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
@ -78,6 +79,7 @@ type Options struct {
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter *regexp.Regexp
Sort int Sort int
Tac bool
Multi bool Multi bool
Mouse bool Mouse bool
Color bool Color bool
@ -102,6 +104,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: nil,
Sort: 1000, Sort: 1000,
Tac: false,
Multi: false, Multi: false,
Mouse: true, Mouse: true,
Color: true, Color: true,
@ -212,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i) opts.Sort = optionalNumeric(allArgs, &i)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "-i": case "-i":
opts.Case = CaseIgnore opts.Case = CaseIgnore
case "+i": case "+i":

View File

@ -22,7 +22,6 @@ import (
type Terminal struct { type Terminal struct {
prompt string prompt string
reverse bool reverse bool
tac bool
cx int cx int
cy int cy int
offset int offset int
@ -85,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
prompt: opts.Prompt, prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse, reverse: opts.Reverse,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
@ -148,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil) 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() { func (t *Terminal) output() {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
@ -162,7 +153,7 @@ func (t *Terminal) output() {
if len(t.selected) == 0 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { 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 { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@ -246,7 +237,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
t.move(i+2, 0, true) t.move(i+2, 0, true)
if i < count { 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() { toggle := func() {
idx := t.listIndex(t.cy) if t.cy < t.merger.Length() {
if idx < t.merger.Length() { item := t.merger.Get(t.cy)
item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found { if _, found := t.selected[item.text]; !found {
var strptr *string var strptr *string
if item.origText != nil { if item.origText != nil {
@ -650,7 +640,7 @@ func (t *Terminal) Loop() {
} else if me.Double { } else if me.Double {
// Double-click // Double-click
if my >= 2 { 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) req(reqClose)
} }
} }

View File

@ -15,7 +15,7 @@ module Temp
waited = 0 waited = 0
while waited < 5 while waited < 5
begin begin
data = File.read(name) data = `cat #{name}`
return data unless data.empty? return data unless data.empty?
rescue rescue
sleep 0.1 sleep 0.1
@ -93,7 +93,7 @@ private
end end
end end
class TestGoFZF < MiniTest::Unit::TestCase class TestGoFZF < Minitest::Test
include Temp include Temp
FIN = 'FIN' FIN = 'FIN'
@ -322,5 +322,28 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
end 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 end

View File

@ -54,7 +54,7 @@ class MockTTY
end end
end end
class TestRubyFZF < MiniTest::Unit::TestCase class TestRubyFZF < Minitest::Test
def setup def setup
ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'