From 50292adacbad70f9561bc1e22ccbd3adea22481a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2015 22:05:02 +0900 Subject: [PATCH] Implement --toggle-sort option (#173) --- CHANGELOG.md | 14 +++++++++++++ README.md | 1 + fzf | 4 +++- install | 2 +- man/man1/fzf.1 | 6 +++++- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- src/constants.go | 2 +- src/core.go | 14 ++++++++----- src/matcher.go | 11 ++++++++-- src/options.go | 35 ++++++++++++++++++++++++++++---- src/options_test.go | 45 +++++++++++++++++++++++++++++++++++++++-- src/pattern.go | 12 +++++++---- src/terminal.go | 17 ++++++++++++++-- test/test_go.rb | 13 ++++++++++++ 16 files changed, 156 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2c957b..d3b1cf32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ 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 ----- diff --git a/README.md b/README.md index d5e58ced..6070f9a1 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ fish. - `CTRL-T` - Paste the selected file path(s) 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 If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You diff --git a/fzf b/fzf index 0cdb115d..69bf14f2 100755 --- a/fzf +++ b/fzf @@ -206,7 +206,9 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--tac', '--sync' + when '--toggle-sort' + argv.shift + when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/ # XXX else usage 1, "illegal option: #{o}" diff --git a/install b/install index eb85a8bc..a8bb0749 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.6 +version=0.9.7 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8c63b3f3..d317adb1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -111,7 +111,7 @@ Print query as the first line .TP .BI "--expect=" "KEY[,..]" 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 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 @@ -120,6 +120,10 @@ with the default enter key. e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE .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" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 90112475..d7f09030 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -44,7 +44,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 --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 bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index ce1eea75..6e9efa44 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -44,7 +44,7 @@ function fzf_key_bindings end 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) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 6eb80839..47806586 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -45,7 +45,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { 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') LBUFFER=!$num zle expand-history diff --git a/src/constants.go b/src/constants.go index 006a1bdc..5cd6d80b 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.6" +const Version = "0.9.7" // fzf events const ( diff --git a/src/core.go b/src/core.go index 204532b2..9f33b41d 100644 --- a/src/core.go +++ b/src/core.go @@ -44,7 +44,7 @@ func initProcs() { /* Reader -> EvtReadFin Reader -> EvtReadNew -> Matcher (restart) -Terminal -> EvtSearchNew -> Matcher (restart) +Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) */ @@ -54,6 +54,7 @@ func Run(options *Options) { initProcs() opts := ParseOptions() + sort := opts.Sort > 0 if opts.Version { fmt.Println(Version) @@ -112,7 +113,7 @@ func Run(options *Options) { } // Reader - streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync + streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} go reader.ReadSource() @@ -123,7 +124,7 @@ func Run(options *Options) { return BuildPattern( 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 if opts.Filter != nil { @@ -190,11 +191,14 @@ func Run(options *Options) { reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) - matcher.Reset(snapshot, terminal.Input(), false, !reading) + matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: + if value.(bool) { + sort = !sort + } snapshot, _ := chunkList.Snapshot() - matcher.Reset(snapshot, terminal.Input(), true, !reading) + matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) delay = false case EvtSearchProgress: diff --git a/src/matcher.go b/src/matcher.go index a3a9bd0e..0f3b409e 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -15,6 +15,7 @@ type MatchRequest struct { chunks []*Chunk pattern *Pattern final bool + sort bool } // Matcher is responsible for performing search @@ -69,6 +70,12 @@ func (m *Matcher) Loop() { events.Clear() }) + if request.sort != m.sort { + m.sort = request.sort + m.mergerCache = make(map[string]*Merger) + clearChunkCache() + } + // Restart search patternString := request.pattern.AsString() var merger *Merger @@ -203,7 +210,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } // 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) var event util.EventType @@ -212,5 +219,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final } else { event = reqRetry } - m.reqBox.Set(event, MatchRequest{chunks, pattern, final}) + m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort}) } diff --git a/src/options.go b/src/options.go index 89b1c368..fcf30979 100644 --- a/src/options.go +++ b/src/options.go @@ -47,6 +47,7 @@ const usage = `usage: fzf [options] -f, --filter=STR Filter mode. Do not start interactive finder. --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf + --toggle-sort=KEY Key to toggle sort --sync Synchronous search for multi-staged filtering (e.g. 'fzf --multi | fzf --sync') @@ -97,6 +98,7 @@ type Options struct { Select1 bool Exit0 bool Filter *string + ToggleSort int Expect []int PrintQuery bool Sync bool @@ -124,6 +126,7 @@ func defaultOptions() *Options { Select1: false, Exit0: false, Filter: nil, + ToggleSort: 0, Expect: []int{}, PrintQuery: false, Sync: false, @@ -201,9 +204,21 @@ func isAlphabet(char uint8) bool { 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 - for _, key := range strings.Split(str, ",") { + for _, key := range tokens { + if len(key) == 0 { + continue // ignore + } lkey := strings.ToLower(key) if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chords = append(chords, curses.CtrlA+int(lkey[5])-'a') @@ -220,6 +235,14 @@ func parseKeyChords(str string) []int { 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) { for i := 0; i < len(allArgs); i++ { arg := allArgs[i] @@ -238,7 +261,9 @@ func parseOptions(opts *Options, allArgs []string) { filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter 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": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": @@ -316,8 +341,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s|--sort="); match { 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 { - opts.Expect = parseKeyChords(value) + opts.Expect = parseKeyChords(value, "key names required") } else { errorExit("unknown option: " + arg) } diff --git a/src/options_test.go b/src/options_test.go index b20cd6a3..36959da4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -70,8 +70,8 @@ func TestIrrelevantNth(t *testing.T) { } } -func TestExpectKeys(t *testing.T) { - keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g") +func TestParseKeys(t *testing.T) { + keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") check := func(key int, expected int) { if 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[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+',') +} diff --git a/src/pattern.go b/src/pattern.go index 7acdbcfa..fbb70c5f 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -54,17 +54,21 @@ var ( ) 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+") - _cache = NewChunkCache() + clearPatternCache() + clearChunkCache() } 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) } +func clearChunkCache() { + _cache = NewChunkCache() +} + // BuildPattern builds Pattern object from the given arguments func BuildPattern(mode Mode, caseMode Case, nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { diff --git a/src/terminal.go b/src/terminal.go index 2d191a90..d027d761 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -28,6 +28,7 @@ type Terminal struct { yanked []rune input []rune multi bool + toggleSort int expect []int pressed int printQuery bool @@ -93,6 +94,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { yanked: []rune{}, input: input, multi: opts.Multi, + toggleSort: opts.ToggleSort, expect: opts.Expect, pressed: 0, printQuery: opts.PrintQuery, @@ -457,6 +459,10 @@ func (t *Terminal) rubout(pattern string) { 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 func (t *Terminal) Loop() { <-t.startChan @@ -553,12 +559,19 @@ func (t *Terminal) Loop() { } } 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 req(reqClose) break } } + if t.toggleSort > 0 { + if keyMatch(t.toggleSort, event) { + t.eventBox.Set(EvtSearchNew, true) + t.mutex.Unlock() + continue + } + } switch event.Type { case C.Invalid: t.mutex.Unlock() @@ -688,7 +701,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { - t.eventBox.Set(EvtSearchNew, nil) + t.eventBox.Set(EvtSearchNew, false) } for _, event := range events { t.reqBox.Set(event, nil) diff --git a/test/test_go.rb b/test/test_go.rb index a47e422e..ebedbff2 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -457,6 +457,19 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter assert_equal ['55', '', '55'], readonce.split($/) 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 module TestShell