diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2c957..d3b1cf3 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 d5e58ce..6070f9a 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 0cdb115..69bf14f 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 eb85a8b..a8bb074 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 8c63b3f..d317adb 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 9011247..d7f0903 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 ce1eea7..6e9efa4 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 6eb8083..4780658 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 006a1bd..5cd6d80 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 204532b..9f33b41 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 a3a9bd0..0f3b409 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 89b1c36..fcf3097 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 b20cd6a..36959da 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 7acdbcf..fbb70c5 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 2d191a9..d027d76 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 a47e422..ebedbff 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