Implement --toggle-sort option (#173)

This commit is contained in:
Junegunn Choi 2015-03-31 22:05:02 +09:00
parent 84a7499ae3
commit 50292adacb
16 changed files with 156 additions and 26 deletions

View File

@ -1,6 +1,20 @@
CHANGELOG CHANGELOG
========= =========
0.9.7
-----
### New features
- Added `--toggle-sort` option (#173)
- `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension
### Bug fixes
- Fixed to print empty line if `--expect` is set and fzf is completed by
`--select-1` or `--exit-0` (#172)
- Fixed to allow comma character as an argument to `--expect` option
0.9.6 0.9.6
----- -----

View File

@ -152,6 +152,7 @@ fish.
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line - `CTRL-R` - Paste the selected command from history into the command line
- Sort is disabled by default. Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You

4
fzf
View File

@ -206,7 +206,9 @@ class FZF
@expect = true @expect = true
when /^--expect=(.*)$/ when /^--expect=(.*)$/
@expect = true @expect = true
when '--tac', '--sync' when '--toggle-sort'
argv.shift
when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/
# XXX # XXX
else else
usage 1, "illegal option: #{o}" usage 1, "illegal option: #{o}"

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.6 version=0.9.7
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)

View File

@ -111,7 +111,7 @@ Print query as the first line
.TP .TP
.BI "--expect=" "KEY[,..]" .BI "--expect=" "KEY[,..]"
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or a single character) that can be used to complete fzf in addition to the or any single character) that can be used to complete fzf in addition to the
default enter key. When this option is set, fzf will print the name of the key default enter key. When this option is set, fzf will print the name of the key
pressed as the first line of its output (or as the second line if pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed \fB--print-query\fR is also used). The line will be empty if fzf is completed
@ -120,6 +120,10 @@ with the default enter key.
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
.TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. ncurses finder only after the input stream is complete.

View File

@ -44,7 +44,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 --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r | 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"'

View File

@ -44,7 +44,7 @@ function fzf_key_bindings
end end
function __fzf_ctrl_r function __fzf_ctrl_r
history | fzf +s +m > $TMPDIR/fzf.result history | fzf +s +m --toggle-sort=ctrl-r > $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

@ -45,7 +45,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() {
local selected local selected
if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. -q "$LBUFFER"); then if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r -q "$LBUFFER"); then
num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g')
LBUFFER=!$num LBUFFER=!$num
zle expand-history zle expand-history

View File

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

View File

@ -44,7 +44,7 @@ func initProcs() {
/* /*
Reader -> EvtReadFin Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart) Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
*/ */
@ -54,6 +54,7 @@ func Run(options *Options) {
initProcs() initProcs()
opts := ParseOptions() opts := ParseOptions()
sort := opts.Sort > 0
if opts.Version { if opts.Version {
fmt.Println(Version) fmt.Println(Version)
@ -112,7 +113,7 @@ func Run(options *Options) {
} }
// Reader // Reader
streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource() go reader.ReadSource()
@ -123,7 +124,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, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@ -190,11 +191,14 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading) terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false, !reading) matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew: case EvtSearchNew:
if value.(bool) {
sort = !sort
}
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading) matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:

View File

@ -15,6 +15,7 @@ type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool final bool
sort bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@ -69,6 +70,12 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
}
// Restart search // Restart search
patternString := request.pattern.AsString() patternString := request.pattern.AsString()
var merger *Merger var merger *Merger
@ -203,7 +210,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@ -212,5 +219,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
} }

View File

@ -47,6 +47,7 @@ const usage = `usage: fzf [options]
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync') (e.g. 'fzf --multi | fzf --sync')
@ -97,6 +98,7 @@ type Options struct {
Select1 bool Select1 bool
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort int
Expect []int Expect []int
PrintQuery bool PrintQuery bool
Sync bool Sync bool
@ -124,6 +126,7 @@ func defaultOptions() *Options {
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: 0,
Expect: []int{}, Expect: []int{},
PrintQuery: false, PrintQuery: false,
Sync: false, Sync: false,
@ -201,9 +204,21 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func parseKeyChords(str string) []int { func parseKeyChords(str string, message string) []int {
if len(str) == 0 {
errorExit(message)
}
tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
tokens = append(tokens, ",")
}
var chords []int var chords []int
for _, key := range strings.Split(str, ",") { for _, key := range tokens {
if len(key) == 0 {
continue // ignore
}
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chords = append(chords, curses.CtrlA+int(lkey[5])-'a') chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
@ -220,6 +235,14 @@ func parseKeyChords(str string) []int {
return chords return chords
} }
func checkToggleSort(str string) int {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
errorExit("multiple keys specified")
}
return keys[0]
}
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
@ -238,7 +261,9 @@ func parseOptions(opts *Options, allArgs []string) {
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--expect": case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required")) opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth": case "-n", "--nth":
@ -316,8 +341,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match { } else if match, _ := optString(arg, "-s|--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match {
opts.ToggleSort = checkToggleSort(value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value) opts.Expect = parseKeyChords(value, "key names required")
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }

View File

@ -70,8 +70,8 @@ func TestIrrelevantNth(t *testing.T) {
} }
} }
func TestExpectKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g") keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) { check := func(key int, expected int) {
if key != expected { if key != expected {
t.Errorf("%d != %d", key, expected) t.Errorf("%d != %d", key, expected)
@ -88,3 +88,44 @@ func TestExpectKeys(t *testing.T) {
check(keys[7], curses.AltZ+'J') check(keys[7], curses.AltZ+'J')
check(keys[8], curses.AltZ+'g') check(keys[8], curses.AltZ+'g')
} }
func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
}
}
keys := parseKeyChords(",", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
keys = parseKeyChords(",,a,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,b,,", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b,c", "")
check(len(keys), 4)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+'c')
check(keys[3], curses.AltZ+',')
keys = parseKeyChords(",,,", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
}

View File

@ -54,17 +54,21 @@ var (
) )
func init() { func init() {
// We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
_splitRegex = regexp.MustCompile("\\s+") _splitRegex = regexp.MustCompile("\\s+")
_cache = NewChunkCache() clearPatternCache()
clearChunkCache()
} }
func clearPatternCache() { func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern) _patternCache = make(map[string]*Pattern)
} }
func clearChunkCache() {
_cache = NewChunkCache()
}
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern {

View File

@ -28,6 +28,7 @@ type Terminal struct {
yanked []rune yanked []rune
input []rune input []rune
multi bool multi bool
toggleSort int
expect []int expect []int
pressed int pressed int
printQuery bool printQuery bool
@ -93,6 +94,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
yanked: []rune{}, yanked: []rune{},
input: input, input: input,
multi: opts.Multi, multi: opts.Multi,
toggleSort: opts.ToggleSort,
expect: opts.Expect, expect: opts.Expect,
pressed: 0, pressed: 0,
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
@ -457,6 +459,10 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...) t.input = append(t.input[:t.cx], after...)
} }
func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
@ -553,12 +559,19 @@ func (t *Terminal) Loop() {
} }
} }
for _, key := range t.expect { for _, key := range t.expect {
if event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ { if keyMatch(key, event) {
t.pressed = key t.pressed = key
req(reqClose) req(reqClose)
break break
} }
} }
if t.toggleSort > 0 {
if keyMatch(t.toggleSort, event) {
t.eventBox.Set(EvtSearchNew, true)
t.mutex.Unlock()
continue
}
}
switch event.Type { switch event.Type {
case C.Invalid: case C.Invalid:
t.mutex.Unlock() t.mutex.Unlock()
@ -688,7 +701,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed { if changed {
t.eventBox.Set(EvtSearchNew, nil) t.eventBox.Set(EvtSearchNew, false)
} }
for _, event := range events { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)

View File

@ -457,6 +457,19 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter
assert_equal ['55', '', '55'], readonce.split($/) assert_equal ['55', '', '55'], readonce.split($/)
end end
def test_toggle_sort
tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.send_keys 'C-R'
tmux.until { |lines| lines[-3].include? '> 11' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (2)' }
tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/)
end
end end
module TestShell module TestShell