From 64443221aab288a3069d01cdaf86706c6c1d91f3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Sep 2015 11:37:55 +0900 Subject: [PATCH] Fix #344 - Backward scan when `--tiebreak=end` --- src/algo/algo.go | 60 +++++++++++++++++++++++++++++-------------- src/algo/algo_test.go | 56 ++++++++++++++++++++++++++-------------- src/core.go | 3 ++- src/pattern.go | 18 +++++++------ src/pattern_test.go | 22 ++++++++-------- test/test_go.rb | 11 ++++++++ 6 files changed, 112 insertions(+), 58 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 03266dd..ac7bd8b 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -15,8 +15,15 @@ import ( * In short: They try to do as little work as possible. */ +func runeAt(runes []rune, index int, max int, forward bool) rune { + if forward { + return runes[index] + } + return runes[max-index-1] +} + // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } @@ -34,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { sidx := -1 eidx := -1 - for index, char := range runes { + lenRunes := len(runes) + lenPattern := len(pattern) + + for index := range runes { + char := runeAt(runes, index, lenRunes, forward) // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -47,11 +58,12 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { char = unicode.To(unicode.LowerCase, char) } } - if char == pattern[pidx] { + pchar := runeAt(pattern, pidx, lenPattern, forward) + if char == pchar { if sidx < 0 { sidx = index } - if pidx++; pidx == len(pattern) { + if pidx++; pidx == lenPattern { eidx = index + 1 break } @@ -61,7 +73,7 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := runes[index] + char := runeAt(runes, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -69,14 +81,19 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { char = unicode.To(unicode.LowerCase, char) } } - if char == pattern[pidx] { + + pchar := runeAt(pattern, pidx, lenPattern, forward) + if char == pchar { if pidx--; pidx < 0 { sidx = index break } } } - return sidx, eidx + if forward { + return sidx, eidx + } + return lenRunes - eidx, lenRunes - sidx } return -1, -1 } @@ -88,20 +105,21 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } - numRunes := len(runes) - plen := len(pattern) - if numRunes < plen { + lenRunes := len(runes) + lenPattern := len(pattern) + + if lenRunes < lenPattern { return -1, -1 } pidx := 0 - for index := 0; index < numRunes; index++ { - char := runes[index] + for index := 0; index < lenRunes; index++ { + char := runeAt(runes, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -109,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int char = unicode.To(unicode.LowerCase, char) } } - if pattern[pidx] == char { + pchar := runeAt(pattern, pidx, lenPattern, forward) + if pchar == char { pidx++ - if pidx == plen { - return index - plen + 1, index + 1 + if pidx == lenPattern { + if forward { + return index - lenPattern + 1, index + 1 + } + return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1) } } else { index -= pidx @@ -123,7 +145,7 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(runes) < len(pattern) { return -1, -1 } @@ -141,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { +func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) { runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) @@ -162,7 +184,7 @@ func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(runes) != len(pattern) { return -1, -1 } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index db24196..95a020b 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -5,11 +5,11 @@ import ( "testing" ) -func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { +func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - s, e := fun(caseSensitive, []rune(input), []rune(pattern)) + s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern)) if s != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) } @@ -19,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSe } func TestFuzzyMatch(t *testing.T) { - assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) + assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestFuzzyMatchBackward(t *testing.T) { + assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4) + assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9) } func TestExactMatchNaive(t *testing.T) { - assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) - assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) - assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1) + } +} + +func TestExactMatchNaiveBackward(t *testing.T) { + assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3) + assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10) } func TestPrefixMatch(t *testing.T) { - assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) - assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3) + assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1) + } } func TestSuffixMatch(t *testing.T) { - assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) - assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9) + assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1) + } } func TestEmptyPattern(t *testing.T) { - assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) - assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) - assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) - assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) + for _, dir := range []bool{true, false} { + assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0) + assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0) + assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0) + assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6) + } } diff --git a/src/core.go b/src/core.go index 4f07215..96bfdd4 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,8 @@ func Run(opts *Options) { // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) + opts.Mode, opts.Case, opts.Tiebreak != byEnd, + opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/pattern.go b/src/pattern.go index cfeb68d..5466b86 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -39,12 +39,13 @@ type term struct { type Pattern struct { mode Mode caseSensitive bool + forward bool text []rune terms []term hasInvTerm bool delimiter Delimiter nth []Range - procFun map[termType]func(bool, []rune, []rune) (int, int) + procFun map[termType]func(bool, bool, []rune, []rune) (int, int) } var ( @@ -70,7 +71,7 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(mode Mode, caseMode Case, +func BuildPattern(mode Mode, caseMode Case, forward bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string @@ -109,12 +110,13 @@ func BuildPattern(mode Mode, caseMode Case, ptr := &Pattern{ mode: mode, caseSensitive: caseSensitive, + forward: forward, text: []rune(asString), terms: terms, hasInvTerm: hasInvTerm, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, []rune, []rune) (int, int))} + procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termEqual] = algo.EqualMatch @@ -288,7 +290,7 @@ func dupItem(item *Item, offsets []Offset) *Item { func (p *Pattern) fuzzyMatch(item *Item) (int, int) { input := p.prepareInput(item) - return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text) + return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) } func (p *Pattern) extendedMatch(item *Item) []Offset { @@ -296,7 +298,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { offsets := []Offset{} for _, term := range p.terms { pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 { + if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { if term.inv { break } @@ -324,11 +326,11 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun func(bool, []rune, []rune) (int, int), - tokens []Token, caseSensitive bool, pattern []rune) (int, int) { +func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int), + tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) { for _, part := range tokens { prefixLength := part.prefixLength - if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { + if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 { return sidx + prefixLength, eidx + prefixLength } } diff --git a/src/pattern_test.go b/src/pattern_test.go index 66f5d41..d508612 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -58,10 +58,10 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( - pattern.caseSensitive, []rune("aabbcc abc"), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -70,11 +70,11 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( - pattern.caseSensitive, []rune(str), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text) if sidx != sidxExpected || eidx != eidxExpected { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -86,17 +86,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -109,7 +109,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize([]rune("junegunn"), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) diff --git a/test/test_go.rb b/test/test_go.rb index 377af22..d1f45dc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -527,6 +527,17 @@ class TestGoFZF < TestBase assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) end + def test_tiebreak_end_backward_scan + input = %w[ + foobar-fb + fubar + ] + writelines tempname, input + + assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/) + assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/) + end + def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' }