summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2015-03-31 22:05:02 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2015-03-31 22:05:16 +0900
commit50292adacbad70f9561bc1e22ccbd3adea22481a (patch)
tree72a9ba657933585b7c56798b71b64a206144421b
parent84a7499ae357bc3b3a82890d2e44d9c300af0c13 (diff)
Implement --toggle-sort option (#173)
-rw-r--r--CHANGELOG.md14
-rw-r--r--README.md1
-rwxr-xr-xfzf4
-rwxr-xr-xinstall2
-rw-r--r--man/man1/fzf.16
-rw-r--r--shell/key-bindings.bash2
-rw-r--r--shell/key-bindings.fish2
-rw-r--r--shell/key-bindings.zsh2
-rw-r--r--src/constants.go2
-rw-r--r--src/core.go14
-rw-r--r--src/matcher.go11
-rw-r--r--src/options.go35
-rw-r--r--src/options_test.go45
-rw-r--r--src/pattern.go12
-rw-r--r--src/terminal.go17
-rw-r--r--test/test_go.rb13
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