From 0f67410bdc3fa86552ea0d5519eefcdc5921f479 Mon Sep 17 00:00:00 2001 From: dhhyi Date: Sun, 12 Nov 2017 10:30:46 +0100 Subject: [PATCH 1/3] respect subtasks in fuzzy search --- lib/fuzzy.go | 28 ++++++++++++++++++---------- lib/fuzzy_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/fuzzy.go b/lib/fuzzy.go index 9ab2371..a82a29d 100644 --- a/lib/fuzzy.go +++ b/lib/fuzzy.go @@ -3,6 +3,7 @@ package sman import ( "github.com/renstrom/fuzzysearch/fuzzy" "sort" + "strings" ) // topsFromRanks iterates through fuzzy.Ranks and returns results @@ -31,19 +32,26 @@ func fSearchFileName(pattern string, dir string) (matched []string) { } // fSearchSnippet matches pattern to snippet name in SnippetSlice -// returnes SnippetSlice of best matched snippets. +// returns SnippetSlice of best matched snippets. func fSearchSnippet(snippets SnippetSlice, pattern string) (matched SnippetSlice) { topRank := -1 for _, s := range snippets { - r := fuzzy.RankMatch(pattern, s.Name) - switch { - case r == -1: - continue - case topRank == -1 || r < topRank: - matched = SnippetSlice{s} - topRank = r - case r == topRank: - matched = append(matched, s) + var queries []string + if strings.Contains(s.Name, ":") { + queries = strings.Split(s.Name, ":") + } + queries = append(queries, s.Name) + for _, part := range queries { + r := fuzzy.RankMatch(pattern, part) + switch { + case r == -1: + continue + case topRank == -1 || r < topRank: + matched = SnippetSlice{s} + topRank = r + case r == topRank: + matched = append(matched, s) + } } } return matched diff --git a/lib/fuzzy_test.go b/lib/fuzzy_test.go index 17a592f..cb17b0b 100644 --- a/lib/fuzzy_test.go +++ b/lib/fuzzy_test.go @@ -64,6 +64,37 @@ func TestFSearchSnippet(t *testing.T) { Snippet{Name: "firbe"}, }, }, + {"multiple matched subtasks", + SnippetSlice{ + Snippet{Name: "user:add"}, + Snippet{Name: "alias:add"}, + Snippet{Name: "non:match"}, + }, "add", + SnippetSlice{ + Snippet{Name: "user:add"}, + Snippet{Name: "alias:add"}, + }, + }, + {"single matched fully qualified", + SnippetSlice{ + Snippet{Name: "user:add"}, + Snippet{Name: "alias:add"}, + Snippet{Name: "non:match"}, + }, "alias:add", + SnippetSlice{ + Snippet{Name: "alias:add"}, + }, + }, + {"single matched fuzzy fully qualified", + SnippetSlice{ + Snippet{Name: "user:add"}, + Snippet{Name: "alias:add"}, + Snippet{Name: "non:match"}, + }, "als:dd", + SnippetSlice{ + Snippet{Name: "alias:add"}, + }, + }, } for _, tt := range tests { if gotMatched := fSearchSnippet(tt.snippets, tt.pattern); !reflect.DeepEqual(gotMatched, tt.wantMatched) { From 45ae62799afab69686382784ab0ff6aae3708a0a Mon Sep 17 00:00:00 2001 From: dhhyi Date: Sun, 12 Nov 2017 10:38:58 +0100 Subject: [PATCH 2/3] also respect subtasks with more than one level in fuzzy search --- lib/fuzzy.go | 35 +++++++++++++++++++++++++++++------ lib/fuzzy_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/lib/fuzzy.go b/lib/fuzzy.go index a82a29d..66f0862 100644 --- a/lib/fuzzy.go +++ b/lib/fuzzy.go @@ -36,12 +36,7 @@ func fSearchFileName(pattern string, dir string) (matched []string) { func fSearchSnippet(snippets SnippetSlice, pattern string) (matched SnippetSlice) { topRank := -1 for _, s := range snippets { - var queries []string - if strings.Contains(s.Name, ":") { - queries = strings.Split(s.Name, ":") - } - queries = append(queries, s.Name) - for _, part := range queries { + for _, part := range nameCombinations(s.Name) { r := fuzzy.RankMatch(pattern, part) switch { case r == -1: @@ -56,3 +51,31 @@ func fSearchSnippet(snippets SnippetSlice, pattern string) (matched SnippetSlice } return matched } + +// construct name combinations using ':' as separator of name parts +// ex: 1:2:3 -> [1, 2, 3, 1:2, 2:3, 1:2:3] +func nameCombinations(pattern string) (combinations []string) { + if len(pattern) > 0 { + if strings.Contains(pattern, ":") { + singleNames := strings.Split(pattern, ":") + + // single names + combinations = append(combinations, singleNames...) + + // combinations with length 2 to n-1 + for combLength := 2; combLength < len(singleNames); combLength++ { + for startAt := 0; startAt < len(singleNames)-combLength+1; startAt++ { + var combination []string + for idx := startAt; idx < startAt+combLength; idx++ { + combination = append(combination, singleNames[idx]) + } + combinations = append(combinations, strings.Join(combination, ":")) + } + } + } + + // complete name + combinations = append(combinations, pattern) + } + return +} diff --git a/lib/fuzzy_test.go b/lib/fuzzy_test.go index cb17b0b..491a472 100644 --- a/lib/fuzzy_test.go +++ b/lib/fuzzy_test.go @@ -95,6 +95,18 @@ func TestFSearchSnippet(t *testing.T) { Snippet{Name: "alias:add"}, }, }, + {"multiple matched subtasks partly qualified", + SnippetSlice{ + Snippet{Name: "mysql:user:add"}, + Snippet{Name: "system:user:add"}, + Snippet{Name: "alias:add"}, + Snippet{Name: "non:match"}, + }, "user:add", + SnippetSlice{ + Snippet{Name: "mysql:user:add"}, + Snippet{Name: "system:user:add"}, + }, + }, } for _, tt := range tests { if gotMatched := fSearchSnippet(tt.snippets, tt.pattern); !reflect.DeepEqual(gotMatched, tt.wantMatched) { @@ -102,3 +114,37 @@ func TestFSearchSnippet(t *testing.T) { } } } + +func TestNameCombinations(t *testing.T) { + tests := []struct { + name string + pattern string + wantCombinations []string + }{ + {"empty name", + "", + []string(nil), + }, + {"single string", + "test", + []string{"test"}, + }, + {"one level subtask", + "one:two", + []string{"one", "two", "one:two"}, + }, + {"two level subtask", + "one:two:three", + []string{"one", "two", "three", "one:two", "two:three", "one:two:three"}, + }, + {"three level subtask", + "one:two:three:four", + []string{"one", "two", "three", "four", "one:two", "two:three", "three:four", "one:two:three", "two:three:four", "one:two:three:four"}, + }, + } + for _, tt := range tests { + if gotCombinations := nameCombinations(tt.pattern); !reflect.DeepEqual(gotCombinations, tt.wantCombinations) { + t.Errorf("%q. nameCombinations() = %v, want %v", tt.name, gotCombinations, tt.wantCombinations) + } + } +} From 1a344578dd39f9bd842c17d6716244b11dbe0f47 Mon Sep 17 00:00:00 2001 From: dhhyi Date: Sat, 18 Nov 2017 18:27:17 +0100 Subject: [PATCH 3/3] return single snippet with whole name match immediately in fuzzy search --- lib/fuzzy.go | 7 +++++++ lib/fuzzy_test.go | 10 ++++++++++ lib/snippet.go | 10 ++++++++++ 3 files changed, 27 insertions(+) diff --git a/lib/fuzzy.go b/lib/fuzzy.go index 66f0862..f11a949 100644 --- a/lib/fuzzy.go +++ b/lib/fuzzy.go @@ -34,6 +34,13 @@ func fSearchFileName(pattern string, dir string) (matched []string) { // fSearchSnippet matches pattern to snippet name in SnippetSlice // returns SnippetSlice of best matched snippets. func fSearchSnippet(snippets SnippetSlice, pattern string) (matched SnippetSlice) { + // special case handling if pattern == snippet name + wholeNameMatchTest := func(s Snippet) bool { return s.Name == pattern } + wholeNameMatch := snippets.FilterView(wholeNameMatchTest) + if wholeNameMatch.Len() == 1 { + return wholeNameMatch + } + topRank := -1 for _, s := range snippets { for _, part := range nameCombinations(s.Name) { diff --git a/lib/fuzzy_test.go b/lib/fuzzy_test.go index 491a472..202aa49 100644 --- a/lib/fuzzy_test.go +++ b/lib/fuzzy_test.go @@ -107,6 +107,16 @@ func TestFSearchSnippet(t *testing.T) { Snippet{Name: "system:user:add"}, }, }, + {"single matched fully qualified special case when whole match applies", + SnippetSlice{ + Snippet{Name: "project:build"}, + Snippet{Name: "project:build:full"}, + Snippet{Name: "non:match"}, + }, "project:build", + SnippetSlice{ + Snippet{Name: "project:build"}, + }, + }, } for _, tt := range tests { if gotMatched := fSearchSnippet(tt.snippets, tt.pattern); !reflect.DeepEqual(gotMatched, tt.wantMatched) { diff --git a/lib/snippet.go b/lib/snippet.go index d973648..11ec489 100644 --- a/lib/snippet.go +++ b/lib/snippet.go @@ -157,3 +157,13 @@ func (s SnippetSlice) Less(a, b int) bool { func (s SnippetSlice) Swap(a, b int) { s[a], s[b] = s[b], s[a] } + +// filter input by test function and return new slice with matched snippets +func (ss SnippetSlice) FilterView(test func(Snippet) bool) (ret SnippetSlice) { + for _, s := range ss { + if test(s) { + ret = append(ret, s) + } + } + return +}