From 81a88693c12507bcc460bd1150af0f48f917670c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 22:49:32 +0900 Subject: [PATCH] Make --extended default Close #400 --- CHANGELOG.md | 9 ++++++++ README.md | 8 +++---- man/man1/fzf.1 | 22 +++++++++---------- shell/completion.bash | 2 +- src/core.go | 2 +- src/options.go | 39 ++++++++++++++++----------------- src/options_test.go | 2 +- src/pattern.go | 50 +++++++++++++++++++++++-------------------- src/pattern_test.go | 28 ++++++++++++------------ test/test_go.rb | 15 +++++++++++-- 10 files changed, 100 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3dd1dc..9fea9735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.10.9 +------ + +- Extended-search mode is now enabled by default + - `--extended-exact` is deprecated and instead we have `--exact` for + orthogonally controlling "exactness" of search +- Fixed not to display non-printable characters +- Added `double-click` for `--bind` option + 0.10.8 ------ diff --git a/README.md b/README.md index ff779ab1..9d0945e4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ vim $(fzf) #### Extended-search mode -With `-x` or `--extended` option, fzf will start in "extended-search mode". +Since 0.10.9, fzf starts in "extended-search mode" by default. In this mode, you can specify multiple patterns delimited by spaces, such as: `^music .mp3$ sbtrkt !rmx` @@ -125,15 +125,15 @@ such as: `^music .mp3$ sbtrkt !rmx` | `!'fire` | Items that do not include `fire` | inverse-exact-match | If you don't prefer fuzzy matching and do not wish to "quote" every word, -start fzf with `-e` or `--extended-exact` option. Note that in -`--extended-exact` mode, `'`-prefix "unquotes" the term. +start fzf with `-e` or `--exact` option. Note that when `--exact` is set, +`'`-prefix "unquotes" the term. #### Environment variables - `FZF_DEFAULT_COMMAND` - Default command to use when input is tty - `FZF_DEFAULT_OPTS` - - Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"` + - Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` Examples -------- diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 62cf960c..200464aa 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Oct 2015" "fzf 0.10.8" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -36,10 +36,11 @@ fzf is a general-purpose command-line fuzzy finder. .SS Search mode .TP .B "-x, --extended" -Extended-search mode +Extended-search mode. Since 0.10.9, this is enabled by default. You can disable +it with \fB+x\fR or \fB--no-extended\fR. .TP -.B "-e, --extended-exact" -Extended-search mode (exact match) +.B "-e, --exact" +Enable exact-match .TP .B "-i" Case-insensitive match (default: smart-case match) @@ -370,9 +371,9 @@ of field index expressions. .SH EXTENDED SEARCH MODE -With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search -mode". In this mode, you can specify multiple patterns delimited by spaces, -such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR +Unless specified otherwise, fzf will start in "extended-search mode". In this +mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild +^music .mp3$ sbtrkt !rmx\fR .SS Exact-match (quoted) A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as @@ -388,11 +389,10 @@ with the given string. An anchored-match term is also an exact-match term. If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the term from the result. -.SS Extended-exact mode +.SS Exact-match by default If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with -\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option -(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR -mode, \fB'\fR-prefix "unquotes" the term. +\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that +when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/shell/completion.bash b/shell/completion.bash index 0c20383a..c8a634db 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -22,7 +22,7 @@ _fzf_opts_completion() { prev="${COMP_WORDS[COMP_CWORD-1]}" opts=" -x --extended - -e --extended-exact + -e --exact -i +i -n --nth -d --delimiter diff --git a/src/core.go b/src/core.go index 35d7cedb..becaed4b 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,7 @@ func Run(opts *Options) { // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Mode, opts.Case, opts.Tiebreak != byEnd, + opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/options.go b/src/options.go index 16de221a..42b27f34 100644 --- a/src/options.go +++ b/src/options.go @@ -16,7 +16,8 @@ const usage = `usage: fzf [options] Search -x, --extended Extended-search mode - -e, --extended-exact Extended-search mode (exact match) + (enabled by default; +x or --no-extended to disable) + -e, --exact Enable Exact-match -i Case-insensitive match (default: smart-case match) +i Case-sensitive match -n, --nth=N[,..] Comma-separated list of field index expressions @@ -58,20 +59,10 @@ const usage = `usage: fzf [options] Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') + FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info') ` -// Mode denotes the current search mode -type Mode int - -// Search modes -const ( - ModeFuzzy Mode = iota - ModeExtended - ModeExtendedExact -) - // Case denotes case-sensitivity of search type Case int @@ -98,7 +89,8 @@ func defaultMargin() [4]string { // Options stores the values of command-line options type Options struct { - Mode Mode + Fuzzy bool + Extended bool Case Case Nth []Range WithNth []Range @@ -143,7 +135,8 @@ func defaultTheme() *curses.ColorTheme { func defaultOptions() *Options { return &Options{ - Mode: ModeFuzzy, + Fuzzy: true, + Extended: true, Case: CaseSmart, Nth: make([]Range, 0), WithNth: make([]Range, 0), @@ -684,11 +677,17 @@ func parseOptions(opts *Options, allArgs []string) { case "-h", "--help": help(exitOk) case "-x", "--extended": - opts.Mode = ModeExtended - case "-e", "--extended-exact": - opts.Mode = ModeExtendedExact - case "+x", "--no-extended", "+e", "--no-extended-exact": - opts.Mode = ModeFuzzy + opts.Extended = true + case "-e", "--exact": + opts.Fuzzy = false + case "--extended-exact": + // Note that we now don't have --no-extended-exact + opts.Fuzzy = false + opts.Extended = true + case "+x", "--no-extended": + opts.Extended = false + case "+e", "--no-exact": + opts.Fuzzy = true case "-q", "--query": opts.Query = nextString(allArgs, &i, "query string required") case "-f", "--filter": @@ -873,7 +872,7 @@ func parseOptions(opts *Options, allArgs []string) { // If we're not using extended search mode, --nth option becomes irrelevant // if it contains the whole range - if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 { + if !opts.Extended || len(opts.Nth) == 1 { for _, r := range opts.Nth { if r.begin == rangeEllipsis && r.end == rangeEllipsis { opts.Nth = make([]Range, 0) diff --git a/src/options_test.go b/src/options_test.go index 1e9ede4e..ef86abec 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -100,7 +100,7 @@ func TestIrrelevantNth(t *testing.T) { t.Errorf("nth should be empty: %s", opts.Nth) } } - for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} { + for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { { opts := defaultOptions() parseOptions(opts, words) diff --git a/src/pattern.go b/src/pattern.go index f5dd8a75..7c81ea02 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -38,7 +38,8 @@ type term struct { // Pattern represents search pattern type Pattern struct { - mode Mode + fuzzy bool + extended bool caseSensitive bool forward bool text []rune @@ -63,7 +64,7 @@ func init() { func clearPatternCache() { // We can uniquely identify the pattern for a given string since - // mode and caseMode do not change while the program is running + // search mode and caseMode do not change while the program is running _patternCache = make(map[string]*Pattern) } @@ -72,14 +73,13 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(mode Mode, caseMode Case, forward bool, +func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string - switch mode { - case ModeExtended, ModeExtendedExact: + if extended { asString = strings.Trim(string(runes), " ") - default: + } else { asString = string(runes) } @@ -91,15 +91,14 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, caseSensitive, hasInvTerm := true, false terms := []term{} - switch mode { - case ModeExtended, ModeExtendedExact: - terms = parseTerms(mode, caseMode, asString) + if extended { + terms = parseTerms(fuzzy, caseMode, asString) for _, term := range terms { if term.inv { hasInvTerm = true } } - default: + } else { lowerString := strings.ToLower(asString) caseSensitive = caseMode == CaseRespect || caseMode == CaseSmart && lowerString != asString @@ -109,7 +108,8 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, } ptr := &Pattern{ - mode: mode, + fuzzy: fuzzy, + extended: extended, caseSensitive: caseSensitive, forward: forward, text: []rune(asString), @@ -129,7 +129,7 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, return ptr } -func parseTerms(mode Mode, caseMode Case, str string) []term { +func parseTerms(fuzzy bool, caseMode Case, str string) []term { tokens := _splitRegex.Split(str, -1) terms := []term{} for _, token := range tokens { @@ -141,7 +141,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { text = lowerText } origText := []rune(text) - if mode == ModeExtendedExact { + if !fuzzy { typ = termExact } @@ -151,10 +151,11 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { } if strings.HasPrefix(text, "'") { - if mode == ModeExtended { + // Flip exactness + if fuzzy { typ = termExact text = text[1:] - } else if mode == ModeExtendedExact { + } else { typ = termFuzzy text = text[1:] } @@ -185,7 +186,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { // IsEmpty returns true if the pattern is effectively empty func (p *Pattern) IsEmpty() bool { - if p.mode == ModeFuzzy { + if !p.extended { return len(p.text) == 0 } return len(p.terms) == 0 @@ -198,7 +199,7 @@ func (p *Pattern) AsString() string { // CacheKey is used to build string to be used as the key of result cache func (p *Pattern) CacheKey() string { - if p.mode == ModeFuzzy { + if !p.extended { return p.AsString() } cacheableTerms := []string{} @@ -250,9 +251,9 @@ Loop: func (p *Pattern) matchChunk(chunk *Chunk) []*Item { matches := []*Item{} - if p.mode == ModeFuzzy { + if !p.extended { for _, item := range *chunk { - if sidx, eidx, tlen := p.fuzzyMatch(item); sidx >= 0 { + if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 { matches = append(matches, dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) } @@ -269,8 +270,8 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item) bool { - if p.mode == ModeFuzzy { - sidx, _, _ := p.fuzzyMatch(item) + if !p.extended { + sidx, _, _ := p.basicMatch(item) return sidx >= 0 } offsets := p.extendedMatch(item) @@ -289,9 +290,12 @@ func dupItem(item *Item, offsets []Offset) *Item { rank: Rank{0, 0, item.index}} } -func (p *Pattern) fuzzyMatch(item *Item) (int, int, int) { +func (p *Pattern) basicMatch(item *Item) (int, int, int) { input := p.prepareInput(item) - return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) + if p.fuzzy { + return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) + } + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) } func (p *Pattern) extendedMatch(item *Item) []Offset { diff --git a/src/pattern_test.go b/src/pattern_test.go index d5086128..8b41a695 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -8,7 +8,7 @@ import ( ) func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(ModeExtended, CaseSmart, + terms := parseTerms(true, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") if len(terms) != 9 || terms[0].typ != termFuzzy || terms[0].inv || @@ -33,7 +33,7 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(ModeExtendedExact, CaseSmart, + terms := parseTerms(false, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || @@ -49,7 +49,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$") + terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } @@ -58,7 +58,7 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, true, + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) @@ -70,7 +70,7 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( @@ -86,17 +86,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -109,19 +109,19 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize([]rune("junegunn"), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) origRunes := []rune("junegunn.choi") - for _, mode := range []Mode{ModeFuzzy, ModeExtended} { + for _, extended := range []bool{false, true} { chunk := Chunk{ &Item{ text: []rune("junegunn"), origText: &origRunes, transformed: trans}, } - pattern.mode = mode + pattern.extended = extended matches := pattern.matchChunk(&chunk) if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || diff --git a/test/test_go.rb b/test/test_go.rb index 77414ecd..50d401c3 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -8,7 +8,7 @@ DEFAULT_TIMEOUT = 20 base = File.expand_path('../../', __FILE__) Dir.chdir base -FZF = "#{base}/bin/fzf" +FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" class NilClass def include? str @@ -213,7 +213,7 @@ class TestGoFZF < TestBase end def test_fzf_default_command - tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Enter @@ -904,6 +904,17 @@ class TestGoFZF < TestBase end end + def test_default_extended + assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp + assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp + end + + def test_exact + assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length + assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length + assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length + end + private def writelines path, lines File.unlink path while File.exists? path