From 4c4b4535ed2bfc917ee90981291e2a15306c28c3 Mon Sep 17 00:00:00 2001 From: Eric F Date: Sun, 20 Nov 2016 17:31:36 +1100 Subject: [PATCH 01/14] Implement #242: fuzzy finder filter - The fuzzy finder is implemented as a new filter, which re-uses the RegexpFilter. - A new queryTransformer field has been added to the RegexpFilter struct to allow changing the user query before applying the filter. - The new queryTransformer is used for the fuzzy filter to change the user query into a regexp. - Also added a test in filter_test.go --- filter.go | 52 ++++++++++++++++++++++++++++++++++++++++++-------- filter_test.go | 38 ++++++++++++++++++++++++++++++++++++ filterutil.go | 14 +++++++++++--- interface.go | 13 ++++++++++++- peco.go | 1 + 5 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 filter_test.go diff --git a/filter.go b/filter.go index 7a13da5..55dbf7a 100644 --- a/filter.go +++ b/filter.go @@ -126,7 +126,7 @@ func (f *Filter) Work(ctx context.Context, q hub.Payload) { g := pdebug.Marker("Periodic draw request for '%s'", query) defer g.End() } - t := time.NewTicker(5*time.Millisecond) + t := time.NewTicker(5 * time.Millisecond) defer t.Stop() defer state.Hub().SendStatusMsg("") defer state.Hub().SendDraw(&DrawOptions{RunningQuery: true}) @@ -199,11 +199,12 @@ func (rf *RegexpFilter) OutCh() <-chan interface{} { func (rf RegexpFilter) Clone() LineFilter { return &RegexpFilter{ - flags: rf.flags, - quotemeta: rf.quotemeta, - query: rf.query, - name: rf.name, - outCh: pipeline.OutputChannel(make(chan interface{})), + flags: rf.flags, + quotemeta: rf.quotemeta, + query: rf.query, + queryTrans: rf.queryTrans, + name: rf.name, + outCh: pipeline.OutputChannel(make(chan interface{})), } } @@ -258,7 +259,7 @@ func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pip defer func() { <-flushDone }() // Wait till the flush goroutine is done defer close(flush) // Kill the flush goroutine - flushTicker := time.NewTicker(50*time.Millisecond) + flushTicker := time.NewTicker(50 * time.Millisecond) defer flushTicker.Stop() start := time.Now() @@ -367,7 +368,7 @@ func (rf *RegexpFilter) getQueryAsRegexps() ([]*regexp.Regexp, error) { if q := rf.compiledQuery; q != nil { return q, nil } - q, err := queryToRegexps(rf.flags, rf.quotemeta, rf.query) + q, err := queryToRegexps(rf.flags, rf.quotemeta, rf.query, rf.queryTrans) if err != nil { return nil, errors.Wrap(err, "failed to compile queries as regular expression") } @@ -420,6 +421,41 @@ func NewSmartCaseFilter() *RegexpFilter { return rf } +// NewFuzzyFilter builds a fuzzy-finder type of filter. +// In effect, this uses a smart case filter, and +// transforms the query from "ABC" to "A(.*)B(.*)C(.*)" +func NewFuzzyFilter() *RegexpFilter { + rf := NewRegexpFilter() + rf.flags = regexpFlagFunc(func(q string) []string { + if util.ContainsUpper(q) { + return defaultFlags + } + return []string{"i"} + }) + rf.quotemeta = true + rf.name = "FuzzySearch" + rf.queryTrans = queryTransformerFunc(func(q string) string { + // Assume that all characters are runes + qr := []rune(q) + res := make([]rune, 5*len(qr)) + i := 0 + for _, r := range qr { + res[i] = r + i++ + res[i] = '(' + i++ + res[i] = '.' + i++ + res[i] = '*' + i++ + res[i] = ')' + i++ + } + return string(res) + }) + return rf +} + func NewExternalCmdFilter(name string, cmd string, args []string, threshold int, idgen lineIDGenerator, enableSep bool) *ExternalCmdFilter { if len(args) == 0 { args = []string{"$QUERY"} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..b9dc911 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,38 @@ +package peco + +import "testing" + +// TestFuzzyFilter tests a fuzzy filter against various inputs +func TestFuzzyFilter(t *testing.T) { + testValues := []struct { + input string + query string + selected bool + }{ + {"this is a test to test the fuzzy Filter", "tf", true}, // normal selection + {"this is a test to test the fuzzy Filter", "wp", false}, // incorrect selection + {"THIS IS A TEST TO TEST THE FUZZY FILTER", "tu", true}, // case insensitivity + {"日本語は難しいです", "難", true}, // kanji + {"あ、日本語は難しいですよ", "あい", true}, // hiragana + {"パソコンは遅いですネ", "ソネ", true}, // katana + {"🚴🏻 abcd efgh", "🚴🏻e", true}, // unicode + } + filter := NewFuzzyFilter() + for i, v := range testValues { + filter.SetQuery(v.query) + l := NewRawLine(uint64(i), v.input, false) + res, err := filter.filter(l) + if v.selected && err != nil { + t.Log("Filtering failed.", "input", v.input, "query", v.query, "err", err) + t.Fail() + } + if v.selected && res == nil { + t.Log("The line should have been selected.", "input", v.input, "query", v.query) + t.Fail() + } + if !v.selected && res != nil { + t.Log("The line should not have been selected.", "input", v.input, "query", v.query) + t.Fail() + } + } +} diff --git a/filterutil.go b/filterutil.go index 8082d81..aaa9a48 100644 --- a/filterutil.go +++ b/filterutil.go @@ -19,12 +19,20 @@ func (r regexpFlagFunc) flags(s string) []string { return r(s) } -func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) { +func (t queryTransformerFunc) transform(s string) string { + return t(s) +} + +func regexpFor(q string, flags []string, quotemeta bool, queryTrans queryTransformer) (*regexp.Regexp, error) { reTxt := q if quotemeta { reTxt = regexp.QuoteMeta(q) } + if queryTrans != nil { + reTxt = queryTrans.transform(reTxt) + } + if flags != nil && len(flags) > 0 { reTxt = fmt.Sprintf("(?%s)%s", strings.Join(flags, ""), reTxt) } @@ -36,12 +44,12 @@ func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) return re, nil } -func queryToRegexps(flags regexpFlags, quotemeta bool, query string) ([]*regexp.Regexp, error) { +func queryToRegexps(flags regexpFlags, quotemeta bool, query string, queryTrans queryTransformer) ([]*regexp.Regexp, error) { queries := strings.Split(strings.TrimSpace(query), " ") regexps := make([]*regexp.Regexp, 0) for _, q := range queries { - re, err := regexpFor(q, flags.flags(query), quotemeta) + re, err := regexpFor(q, flags.flags(query), quotemeta, queryTrans) if err != nil { return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", q) } diff --git a/interface.go b/interface.go index c70db8a..b1adf2e 100644 --- a/interface.go +++ b/interface.go @@ -297,6 +297,16 @@ type regexpFlagList []string type regexpFlagFunc func(string) []string +// queryTransformer is able to transform a query from one form to another. +// This is used by the FuzzyFilter to transform the user query to a regular +// expression. +type queryTransformer interface { + transform(string) string +} + +// type queryTransformerFunc implements queryTransformer. +type queryTransformerFunc func(string) string + // Filter is responsible for the actual "grep" part of peco type Filter struct { state *Peco @@ -324,7 +334,7 @@ type FilteredBuffer struct { } // Config holds all the data that can be configured in the -// external configuran file +// external configuration file type Config struct { Action map[string][]string `json:"Action"` // Keymap used to be directly responsible for dispatching @@ -518,6 +528,7 @@ type RegexpFilter struct { flags regexpFlags quotemeta bool query string + queryTrans queryTransformer mutex sync.Mutex name string onEnd func() diff --git a/peco.go b/peco.go index 0fcb49c..095028a 100644 --- a/peco.go +++ b/peco.go @@ -566,6 +566,7 @@ func (p *Peco) populateFilters() error { p.filters.Add(NewCaseSensitiveFilter()) p.filters.Add(NewSmartCaseFilter()) p.filters.Add(NewRegexpFilter()) + p.filters.Add(NewFuzzyFilter()) for name, c := range p.config.CustomFilter { f := NewExternalCmdFilter(name, c.Cmd, c.Args, c.BufferThreshold, p.idgen, p.enableSep) From 21179c2c38f24bc0d5ff2dd41261fa0ac90a6ac1 Mon Sep 17 00:00:00 2001 From: Eric F Date: Thu, 8 Dec 2016 17:20:55 +1100 Subject: [PATCH 02/14] Implement #242: Made the fuzzy filter configurable --- README.md | 22 ++++++++++++++++------ config.go | 1 + config_test.go | 1 + filter.go | 3 +-- interface.go | 13 +++++++++++++ peco.go | 21 ++++++++++++++++++++- peco_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 90 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4c30cdc..915d8c6 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,15 @@ Not only can you select multiple lines one by one, you can select a range of lin ## Select Filters -Different types of filters are available. Default is case-insensitive filter, so lines with any case will match. You can toggle between IgnoreCase, CaseSensitive, SmartCase and RegExp filters. +Different types of filters are available. Default is case-insensitive filter, so lines with any case will match. You can toggle between IgnoreCase, CaseSensitive, SmartCase RegExp and Fuzzy filters. The SmartCase filter uses case-*insensitive* matching when all of the queries are lower case, and case-*sensitive* matching otherwise. The RegExp filter allows you to use any valid regular expression to match lines -![Executed `ps aux | peco`, then typed `google`, which matches the Chrome.app under IgnoreCase filter type. Whenyou change it to Regexp filter, this is no longer the case. But you can type `(?i)google` instead to toggle case-insensitive mode](http://peco.github.io/images/peco-demo-matcher.gif) +The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using option `--enable-fuzzy`. + +![Executed `ps aux | peco`, then typed `google`, which matches the Chrome.app under IgnoreCase filter type. When you change it to Regexp filter, this is no longer the case. But you can type `(?i)google` instead to toggle case-insensitive mode](http://peco.github.io/images/peco-demo-matcher.gif) ## Selectable Layout @@ -158,9 +160,13 @@ Changes how peco interprets incoming data. When this flag is set, you may insert Specifies the initial line position upon start up. E.g. If you want to start out with the second line selected, set it to "1" (because the index is 0 based) -### --initial-filter `IgnoreCase|CaseSensitive|SmartCase|Regexp` +### --fuzzy-filter `enabled|disabled` -Specifies the initial filter to use upon start up. You should specify the name of the filter like `IgnoreCase`, `CaseSensitive`, `SmartCase` and `Regexp`. Default is `IgnoreCase`. +Make the Fuzzy filter available. The command line option can override the value in the configuration file. Default is `disabled`. + +### --initial-filter `IgnoreCase|CaseSensitive|SmartCase|Regexp|Fuzzy` + +Specifies the initial filter to use upon start up. You should specify the name of the filter like `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy` (if enabled). Default is `IgnoreCase`. ### --prompt @@ -217,9 +223,13 @@ You can change the query line's prompt, which is `QUERY>` by default. *InitialMatcher* has been deprecated. Please use `InitialFilter` instead. +### FuzzyFilter + +Set to `enabled` to make the Fuzzy filter available. Default is `disabled`. + ### InitialFilter -Specifies the filter name to start peco with. You should specify the name of the filter, such as `IgnoreCase`, `CaseSensitive`, `SmartCase` and `Regexp` +Specifies the filter name to start peco with. You should specify the name of the filter, such as `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy` (if [enabled](#FuzzyFilter)). ### StickySelection @@ -473,7 +483,7 @@ For now, styles of following 5 items can be customized in `config.json`. This is an experimental feature. Please note that some details of this specification may change -By default `peco` comes with `IgnoreCase`, `CaseSensitive`, `SmartCase` and `Regexp` filters, but since v0.1.3, it is possible to create your own custom filter. +By default `peco` comes with `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy` filters, but since v0.1.3, it is possible to create your own custom filter. The filter will be executed via `Command.Run()` as an external process, and it will be passed the query values in the command line, and the original unaltered buffer is passed via `os.Stdin`. Your filter must perform the matching, and print out to `os.Stdout` matched lines. You filter MAY be called multiple times if the buffer given to peco is big enough. See `BufferThreshold` below. diff --git a/config.go b/config.go index 586ab35..b42428d 100644 --- a/config.go +++ b/config.go @@ -25,6 +25,7 @@ func (c *Config) Init() error { c.Style.Init() c.Prompt = "QUERY>" c.Layout = LayoutTypeTopDown + c.FuzzyFilter = OptionDisabled return nil } diff --git a/config_test.go b/config_test.go index 7c94d0b..f050f63 100644 --- a/config_test.go +++ b/config_test.go @@ -47,6 +47,7 @@ func TestReadRC(t *testing.T) { InitialMatcher: IgnoreCaseMatch, Layout: DefaultLayoutType, Prompt: "[peco]", + FuzzyFilter: "disabled", Style: StyleSet{ Matched: Style{ fg: termbox.ColorCyan | termbox.AttrBold, diff --git a/filter.go b/filter.go index 55dbf7a..5d300b3 100644 --- a/filter.go +++ b/filter.go @@ -433,9 +433,8 @@ func NewFuzzyFilter() *RegexpFilter { return []string{"i"} }) rf.quotemeta = true - rf.name = "FuzzySearch" + rf.name = FuzzyFilter rf.queryTrans = queryTransformerFunc(func(q string) string { - // Assume that all characters are runes qr := []rune(q) res := make([]rune, 5*len(qr)) i := 0 diff --git a/interface.go b/interface.go index b1adf2e..f8a3308 100644 --- a/interface.go +++ b/interface.go @@ -46,6 +46,11 @@ const ( RegexpMatch = "Regexp" ) +// Filter names, used in the config file +const ( + FuzzyFilter = "Fuzzy" +) + // lineIDGenerator defines an interface for things that generate // unique IDs for lines used within peco. type lineIDGenerator interface { @@ -56,6 +61,11 @@ type idgen struct { ch chan uint64 } +const ( + OptionEnabled = "enabled" + OptionDisabled = "disabled" +) + // Peco is the global object containing everything required to run peco. // It also contains the global state of the program. type Peco struct { @@ -74,6 +84,7 @@ type Peco struct { enableSep bool // Enable parsing on separators filters FilterSet idgen *idgen + enableFuzzy bool initialFilter string initialQuery string // populated if --query is specified inputseq Inputseq // current key sequence (just the names) @@ -343,6 +354,7 @@ type Config struct { Keymap map[string]string `json:"Keymap"` Matcher string `json:"Matcher"` // Deprecated. InitialMatcher string `json:"InitialMatcher"` // Use this instead of Matcher + FuzzyFilter string `json:"FuzzyFilter"` InitialFilter string `json:"InitialFilter"` Style StyleSet `json:"Style"` Prompt string `json:"Prompt"` @@ -475,6 +487,7 @@ type CLIOptions struct { OptEnableNullSep bool `long:"null" description:"expect NUL (\\0) as separator for target/output"` OptInitialIndex int `long:"initial-index" description:"position of the initial index of the selection (0 base)"` OptInitialMatcher string `long:"initial-matcher" description:"specify the default matcher (deprecated)"` + OptFuzzyFilter string `short:"z" long:"fuzzy-filter" description:"enable/disable the Fuzzy filter"` OptInitialFilter string `long:"initial-filter" description:"specify the default filter"` OptPrompt string `long:"prompt" description:"specify the prompt string"` OptLayout string `long:"layout" description:"layout to be used 'top-down' or 'bottom-up'. default is 'top-down'"` diff --git a/peco.go b/peco.go index 095028a..3c21224 100644 --- a/peco.go +++ b/peco.go @@ -500,6 +500,20 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { p.bufferSize = opts.OptBufferSize p.selectOneAndExit = opts.OptSelect1 p.initialQuery = opts.OptQuery + // Option EnableFuzzy is a string to allow overriding the value on the command line + fuzzyFilter := opts.OptFuzzyFilter + if len(fuzzyFilter) <= 0 { + fuzzyFilter = p.config.FuzzyFilter + } + if len(fuzzyFilter) > 0 { + if fuzzyFilter == OptionEnabled { + p.enableFuzzy = true + } else if fuzzyFilter == OptionDisabled { + p.enableFuzzy = false + } else { + return errors.Errorf("Unexpected value for FuzzyFilter option: %v (expected %v/%v)", fuzzyFilter, OptionEnabled, OptionDisabled) + } + } p.initialFilter = opts.OptInitialFilter if len(p.initialFilter) <= 0 { p.initialFilter = p.config.InitialFilter @@ -507,6 +521,9 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { if len(p.initialFilter) <= 0 { p.initialFilter = opts.OptInitialMatcher } + if len(p.initialFilter) > 0 && !p.enableFuzzy && p.initialFilter == FuzzyFilter { + return errors.New("Fuzzy filter is not enabled, can not set it to the initial filter.") + } if err := p.populateCommandList(); err != nil { return errors.Wrap(err, "failed to populate command list") @@ -566,7 +583,9 @@ func (p *Peco) populateFilters() error { p.filters.Add(NewCaseSensitiveFilter()) p.filters.Add(NewSmartCaseFilter()) p.filters.Add(NewRegexpFilter()) - p.filters.Add(NewFuzzyFilter()) + if p.enableFuzzy { + p.filters.Add(NewFuzzyFilter()) + } for name, c := range p.config.CustomFilter { f := NewExternalCmdFilter(name, c.Cmd, c.Args, c.BufferThreshold, p.idgen, p.enableSep) diff --git a/peco_test.go b/peco_test.go index c486055..edef95c 100644 --- a/peco_test.go +++ b/peco_test.go @@ -197,6 +197,44 @@ func TestGHIssue331(t *testing.T) { } } +func TestConfigFuzzyFilter(t *testing.T) { + var opts CLIOptions + p := newPeco() + + // Ensure that it's possible to enable the Fuzzy filter + opts.OptFuzzyFilter = "enabled" + if !assert.NoError(t, p.ApplyConfig(opts), "p.ApplyConfig should succeed") { + return + } + if !assert.Equal(t, true, p.enableFuzzy, "p.enableFuzzy should be equal to opts.OptEnableFuzzy") { + return + } + opts.OptFuzzyFilter = "abc" + if !assert.Error(t, p.ApplyConfig(opts), "p.ApplyConfig should not succeed") { + return + } +} + +func TestConfigInitialFilterFuzzy(t *testing.T) { + var opts CLIOptions + p := newPeco() + + // If Fuzzy is not enabled, initialFilter=Fuzzy should cause peco to fail + opts.OptFuzzyFilter = "disabled" + opts.OptInitialFilter = "Fuzzy" + if !assert.Error(t, p.ApplyConfig(opts), "p.ApplyConfig should not succeed") { + return + } + // If Fuzzy is enabled, it should be possible to set the initial filter to Fuzzy + opts.OptFuzzyFilter = "enabled" + if !assert.NoError(t, p.ApplyConfig(opts), "p.ApplyConfig should succeed") { + return + } + if !assert.Equal(t, true, p.enableFuzzy, "p.enableFuzzy should be equal to opts.OptEnableFuzzy") { + return + } +} + func TestApplyConfig(t *testing.T) { // XXX We should add all the possible configurations that needs to be // propagated to Peco from config From 1fb6e3b1b7aba46f3335441a29cdfd625881caec Mon Sep 17 00:00:00 2001 From: Eric F Date: Thu, 8 Dec 2016 23:22:01 +1100 Subject: [PATCH 03/14] Implement #242: Use switch case Replace if statement with switch when validating the FuzzyFilter option. --- peco.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/peco.go b/peco.go index 3c21224..a957b8f 100644 --- a/peco.go +++ b/peco.go @@ -500,17 +500,17 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { p.bufferSize = opts.OptBufferSize p.selectOneAndExit = opts.OptSelect1 p.initialQuery = opts.OptQuery - // Option EnableFuzzy is a string to allow overriding the value on the command line fuzzyFilter := opts.OptFuzzyFilter if len(fuzzyFilter) <= 0 { fuzzyFilter = p.config.FuzzyFilter } if len(fuzzyFilter) > 0 { - if fuzzyFilter == OptionEnabled { + switch fuzzyFilter { + case OptionEnabled: p.enableFuzzy = true - } else if fuzzyFilter == OptionDisabled { + case OptionDisabled: p.enableFuzzy = false - } else { + default: return errors.Errorf("Unexpected value for FuzzyFilter option: %v (expected %v/%v)", fuzzyFilter, OptionEnabled, OptionDisabled) } } From c00f0d29753fd26e7840c7c3834d92ddfe6f8d67 Mon Sep 17 00:00:00 2001 From: Eric F Date: Thu, 8 Dec 2016 23:35:09 +1100 Subject: [PATCH 04/14] Implement #242: Add case sensitivity test cases --- filter_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/filter_test.go b/filter_test.go index b9dc911..e53f2b8 100644 --- a/filter_test.go +++ b/filter_test.go @@ -12,6 +12,10 @@ func TestFuzzyFilter(t *testing.T) { {"this is a test to test the fuzzy Filter", "tf", true}, // normal selection {"this is a test to test the fuzzy Filter", "wp", false}, // incorrect selection {"THIS IS A TEST TO TEST THE FUZZY FILTER", "tu", true}, // case insensitivity + {"this is a Test to test the fuzzy filter", "Tu", true}, // case sensitivity + {"this is a Test to test the fUzzy filter", "TU", true}, // case sensitivity + {"this is a test to test the fuzzy filter", "Tu", false}, // case sensitivity + {"this is a test to Test the fuzzy filter", "TU", false}, // case sensitivity {"日本語は難しいです", "難", true}, // kanji {"あ、日本語は難しいですよ", "あい", true}, // hiragana {"パソコンは遅いですネ", "ソネ", true}, // katana From 99f30cad6cb568ac9899dda168ca08b0514bd741 Mon Sep 17 00:00:00 2001 From: Eric F Date: Sun, 11 Dec 2016 13:56:50 +1100 Subject: [PATCH 05/14] Add more details to README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 915d8c6..3337ea5 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The SmartCase filter uses case-*insensitive* matching when all of the queries ar The RegExp filter allows you to use any valid regular expression to match lines -The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using option `--enable-fuzzy`. +The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using option `--fuzzy-filter`. ![Executed `ps aux | peco`, then typed `google`, which matches the Chrome.app under IgnoreCase filter type. When you change it to Regexp filter, this is no longer the case. But you can type `(?i)google` instead to toggle case-insensitive mode](http://peco.github.io/images/peco-demo-matcher.gif) @@ -225,7 +225,7 @@ You can change the query line's prompt, which is `QUERY>` by default. ### FuzzyFilter -Set to `enabled` to make the Fuzzy filter available. Default is `disabled`. +Set to `enabled` to make the Fuzzy filter available, or `disabled` to disable it. Default is `disabled`. ### InitialFilter From bfb3ae2fc101dde62ca461c0458d23cd91295469 Mon Sep 17 00:00:00 2001 From: Eric F Date: Sun, 11 Dec 2016 14:00:44 +1100 Subject: [PATCH 06/14] Fix english in README fuzzy filter description. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3337ea5..5581063 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The SmartCase filter uses case-*insensitive* matching when all of the queries ar The RegExp filter allows you to use any valid regular expression to match lines -The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using option `--fuzzy-filter`. +The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using the option `--fuzzy-filter`. ![Executed `ps aux | peco`, then typed `google`, which matches the Chrome.app under IgnoreCase filter type. When you change it to Regexp filter, this is no longer the case. But you can type `(?i)google` instead to toggle case-insensitive mode](http://peco.github.io/images/peco-demo-matcher.gif) From 2dbfd7820525bbbd04f6a6b89c6bf739afa1521b Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Mon, 12 Dec 2016 17:12:07 +0900 Subject: [PATCH 07/14] Rework #369 * --fuzzy-filter seems unnecessary * regexp.Regexp is overkill. Implement in terms of Index*() --- README.md | 14 +---- filter.go | 160 +++++++++++++++++++++++++++++++++---------------- filter_test.go | 46 +++++++++----- interface.go | 11 ++-- peco.go | 21 +------ peco_test.go | 27 --------- 6 files changed, 147 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 5581063..16adc65 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The SmartCase filter uses case-*insensitive* matching when all of the queries ar The RegExp filter allows you to use any valid regular expression to match lines -The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. The Fuzzy filter is not enabled by default, but can be enabled using the option `--fuzzy-filter`. +The Fuzzy filter allows you to find matches using partial patterns. For example, when searching for `ALongString`, you can enable the Fuzzy filter and search `ALS` to find it. The Fuzzy filter uses smart case search like the SmartCase filter. ![Executed `ps aux | peco`, then typed `google`, which matches the Chrome.app under IgnoreCase filter type. When you change it to Regexp filter, this is no longer the case. But you can type `(?i)google` instead to toggle case-insensitive mode](http://peco.github.io/images/peco-demo-matcher.gif) @@ -160,13 +160,9 @@ Changes how peco interprets incoming data. When this flag is set, you may insert Specifies the initial line position upon start up. E.g. If you want to start out with the second line selected, set it to "1" (because the index is 0 based) -### --fuzzy-filter `enabled|disabled` - -Make the Fuzzy filter available. The command line option can override the value in the configuration file. Default is `disabled`. - ### --initial-filter `IgnoreCase|CaseSensitive|SmartCase|Regexp|Fuzzy` -Specifies the initial filter to use upon start up. You should specify the name of the filter like `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy` (if enabled). Default is `IgnoreCase`. +Specifies the initial filter to use upon start up. You should specify the name of the filter like `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy`. Default is `IgnoreCase`. ### --prompt @@ -223,13 +219,9 @@ You can change the query line's prompt, which is `QUERY>` by default. *InitialMatcher* has been deprecated. Please use `InitialFilter` instead. -### FuzzyFilter - -Set to `enabled` to make the Fuzzy filter available, or `disabled` to disable it. Default is `disabled`. - ### InitialFilter -Specifies the filter name to start peco with. You should specify the name of the filter, such as `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy` (if [enabled](#FuzzyFilter)). +Specifies the filter name to start peco with. You should specify the name of the filter, such as `IgnoreCase`, `CaseSensitive`, `SmartCase`, `Regexp` and `Fuzzy`. ### StickySelection diff --git a/filter.go b/filter.go index 5d300b3..a424292 100644 --- a/filter.go +++ b/filter.go @@ -6,8 +6,11 @@ import ( "os/exec" "regexp" "sort" + "strings" "sync" "time" + "unicode" + "unicode/utf8" "github.com/lestrrat/go-pdebug" "github.com/peco/peco/hub" @@ -216,7 +219,7 @@ var filterBufPool = sync.Pool{ }, } -func releaseRegexpFilterBuf(l []Line) { +func releaseFilterLineBuf(l []Line) { if l == nil { return } @@ -224,38 +227,51 @@ func releaseRegexpFilterBuf(l []Line) { filterBufPool.Put(l) } -func getRegexpFilterBuf() []Line { +func getFilterLineBuf() []Line { l := filterBufPool.Get().([]Line) return l } +type filter interface { + filter(Line) (Line, error) +} + +// This flusher is run in a separate goroutine so that the filter can +// run separately from accepting incoming messages +func flusher(f filter, incoming chan []Line, done chan struct{}, out pipeline.OutputChannel) { + if pdebug.Enabled { + g := pdebug.Marker("flusher goroutine") + defer g.End() + } + + defer close(done) + defer out.SendEndMark("end of filter") + for buf := range incoming { + for _, in := range buf { + if l, err := f.filter(in); err == nil { + out.Send(l) + } + } + releaseFilterLineBuf(buf) + } +} + func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { if pdebug.Enabled { g := pdebug.Marker("RegexpFilter.Accept") defer g.End() } + filterAcceptAndFilter(ctx, rf, in, out) +} + +func filterAcceptAndFilter(ctx context.Context, f filter, in chan interface{}, out pipeline.OutputChannel) { flush := make(chan []Line) flushDone := make(chan struct{}) - go func() { - if pdebug.Enabled { - g := pdebug.Marker("RegexpFilter.Accept flusher goroutine") - defer g.End() - } - defer close(flushDone) - defer out.SendEndMark("end of RegexpFilter") - for buf := range flush { - for _, in := range buf { - if l, err := rf.filter(in); err == nil { - out.Send(l) - } - } - releaseRegexpFilterBuf(buf) - } - }() + go flusher(f, flush, flushDone, out) - buf := getRegexpFilterBuf() - defer func() { releaseRegexpFilterBuf(buf) }() + buf := getFilterLineBuf() + defer func() { releaseFilterLineBuf(buf) }() defer func() { <-flushDone }() // Wait till the flush goroutine is done defer close(flush) // Kill the flush goroutine @@ -268,7 +284,7 @@ func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pip select { case <-ctx.Done(): if pdebug.Enabled { - pdebug.Printf("RegexpFilter received done") + pdebug.Printf("filter received done") } return case v := <-in: @@ -276,7 +292,7 @@ func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pip case error: if pipeline.IsEndMark(v.(error)) { if pdebug.Enabled { - pdebug.Printf("RegexpFilter received end mark (read %d lines, %s since starting accept loop)", lines+len(buf), time.Since(start).String()) + pdebug.Printf("filter received end mark (read %d lines, %s since starting accept loop)", lines+len(buf), time.Since(start).String()) } if len(buf) > 0 { flush <- buf @@ -296,11 +312,11 @@ func (rf *RegexpFilter) Accept(ctx context.Context, in chan interface{}, out pip select { case <-flushTicker.C: flush <- buf - buf = getRegexpFilterBuf() + buf = getFilterLineBuf() default: if len(buf) >= cap(buf) { flush <- buf - buf = getRegexpFilterBuf() + buf = getFilterLineBuf() } } } @@ -423,36 +439,76 @@ func NewSmartCaseFilter() *RegexpFilter { // NewFuzzyFilter builds a fuzzy-finder type of filter. // In effect, this uses a smart case filter, and -// transforms the query from "ABC" to "A(.*)B(.*)C(.*)" -func NewFuzzyFilter() *RegexpFilter { - rf := NewRegexpFilter() - rf.flags = regexpFlagFunc(func(q string) []string { - if util.ContainsUpper(q) { - return defaultFlags +// transforms the query from "ABC" to the equivalent of "A(.*)B(.*)C(.*)" +func NewFuzzyFilter() *FuzzyFilter { + return &FuzzyFilter{} +} + +func (ff FuzzyFilter) Clone() LineFilter { + return &FuzzyFilter{ + query: ff.query, + } +} + +func (ff *FuzzyFilter) SetQuery(q string) { + ff.mutex.Lock() + defer ff.mutex.Unlock() + + ff.query = q +} + +func (ff FuzzyFilter) String() string { + return "Fuzzy" +} + +func (ff *FuzzyFilter) Accept(ctx context.Context, in chan interface{}, out pipeline.OutputChannel) { + if pdebug.Enabled { + g := pdebug.Marker("FuzzyFilter.Accept") + defer g.End() + } + + filterAcceptAndFilter(ctx, ff, in, out) +} + +func (ff *FuzzyFilter) filter(l Line) (Line, error) { + query := "" + ff.mutex.Lock() + query = ff.query + ff.mutex.Unlock() + + base := 0 + txt := l.DisplayString() + matches := [][]int{} + + hasUpper := strings.IndexFunc(query, unicode.IsUpper) > -1 + + for len(query) > 0 { + r, n := utf8.DecodeRuneInString(query) + if r == utf8.RuneError { + // "Silently" ignore (just return a no match) + return nil, errors.New("failed to decode input string") } - return []string{"i"} - }) - rf.quotemeta = true - rf.name = FuzzyFilter - rf.queryTrans = queryTransformerFunc(func(q string) string { - qr := []rune(q) - res := make([]rune, 5*len(qr)) - i := 0 - for _, r := range qr { - res[i] = r - i++ - res[i] = '(' - i++ - res[i] = '.' - i++ - res[i] = '*' - i++ - res[i] = ')' - i++ + query = query[n:] + + var i int + if hasUpper { // explicit match + i = strings.IndexRune(txt, r) + } else { + i = strings.IndexFunc(txt, func(v rune) bool { + return unicode.ToUpper(r) == v || unicode.ToLower(r) == v + }) } - return string(res) - }) - return rf + if i == -1 { + return nil, errors.New("filter did not match against given line") + } + + // otherwise we have a match, but the next match must match against + // something AFTER the current match + txt = txt[i+n:] + matches = append(matches, []int{base + i, base+i+n}) + base = base + i + n + } + return NewMatchedLine(l, matches), nil } func NewExternalCmdFilter(name string, cmd string, args []string, threshold int, idgen lineIDGenerator, enableSep bool) *ExternalCmdFilter { diff --git a/filter_test.go b/filter_test.go index e53f2b8..6eca22c 100644 --- a/filter_test.go +++ b/filter_test.go @@ -1,6 +1,11 @@ package peco -import "testing" +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) // TestFuzzyFilter tests a fuzzy filter against various inputs func TestFuzzyFilter(t *testing.T) { @@ -23,20 +28,29 @@ func TestFuzzyFilter(t *testing.T) { } filter := NewFuzzyFilter() for i, v := range testValues { - filter.SetQuery(v.query) - l := NewRawLine(uint64(i), v.input, false) - res, err := filter.filter(l) - if v.selected && err != nil { - t.Log("Filtering failed.", "input", v.input, "query", v.query, "err", err) - t.Fail() - } - if v.selected && res == nil { - t.Log("The line should have been selected.", "input", v.input, "query", v.query) - t.Fail() - } - if !v.selected && res != nil { - t.Log("The line should not have been selected.", "input", v.input, "query", v.query) - t.Fail() - } + t.Run(fmt.Sprintf(`"%s" against "%s", expect "%t"`, v.input, v.query, v.selected), func(t *testing.T) { + filter.SetQuery(v.query) + l := NewRawLine(uint64(i), v.input, false) + res, err := filter.filter(l) + + if !v.selected { + if !assert.Error(t, err, "filter should fail") { + return + } + if !assert.Nil(t, res, "return value should be nil") { + return + } + return + } + + if !assert.NoError(t, err, "filtering failed") { + return + } + + if !assert.NotNil(t, res, "return value should NOT be nil") { + return + } + t.Logf("%#v", res.Indices()) + }) } } diff --git a/interface.go b/interface.go index f8a3308..37de986 100644 --- a/interface.go +++ b/interface.go @@ -46,11 +46,6 @@ const ( RegexpMatch = "Regexp" ) -// Filter names, used in the config file -const ( - FuzzyFilter = "Fuzzy" -) - // lineIDGenerator defines an interface for things that generate // unique IDs for lines used within peco. type lineIDGenerator interface { @@ -487,7 +482,6 @@ type CLIOptions struct { OptEnableNullSep bool `long:"null" description:"expect NUL (\\0) as separator for target/output"` OptInitialIndex int `long:"initial-index" description:"position of the initial index of the selection (0 base)"` OptInitialMatcher string `long:"initial-matcher" description:"specify the default matcher (deprecated)"` - OptFuzzyFilter string `short:"z" long:"fuzzy-filter" description:"enable/disable the Fuzzy filter"` OptInitialFilter string `long:"initial-filter" description:"specify the default filter"` OptPrompt string `long:"prompt" description:"specify the prompt string"` OptLayout string `long:"layout" description:"layout to be used 'top-down' or 'bottom-up'. default is 'top-down'"` @@ -536,6 +530,11 @@ type LineFilter interface { String() string } +type FuzzyFilter struct { + mutex sync.Mutex + query string +} + type RegexpFilter struct { compiledQuery []*regexp.Regexp flags regexpFlags diff --git a/peco.go b/peco.go index a957b8f..095028a 100644 --- a/peco.go +++ b/peco.go @@ -500,20 +500,6 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { p.bufferSize = opts.OptBufferSize p.selectOneAndExit = opts.OptSelect1 p.initialQuery = opts.OptQuery - fuzzyFilter := opts.OptFuzzyFilter - if len(fuzzyFilter) <= 0 { - fuzzyFilter = p.config.FuzzyFilter - } - if len(fuzzyFilter) > 0 { - switch fuzzyFilter { - case OptionEnabled: - p.enableFuzzy = true - case OptionDisabled: - p.enableFuzzy = false - default: - return errors.Errorf("Unexpected value for FuzzyFilter option: %v (expected %v/%v)", fuzzyFilter, OptionEnabled, OptionDisabled) - } - } p.initialFilter = opts.OptInitialFilter if len(p.initialFilter) <= 0 { p.initialFilter = p.config.InitialFilter @@ -521,9 +507,6 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { if len(p.initialFilter) <= 0 { p.initialFilter = opts.OptInitialMatcher } - if len(p.initialFilter) > 0 && !p.enableFuzzy && p.initialFilter == FuzzyFilter { - return errors.New("Fuzzy filter is not enabled, can not set it to the initial filter.") - } if err := p.populateCommandList(); err != nil { return errors.Wrap(err, "failed to populate command list") @@ -583,9 +566,7 @@ func (p *Peco) populateFilters() error { p.filters.Add(NewCaseSensitiveFilter()) p.filters.Add(NewSmartCaseFilter()) p.filters.Add(NewRegexpFilter()) - if p.enableFuzzy { - p.filters.Add(NewFuzzyFilter()) - } + p.filters.Add(NewFuzzyFilter()) for name, c := range p.config.CustomFilter { f := NewExternalCmdFilter(name, c.Cmd, c.Args, c.BufferThreshold, p.idgen, p.enableSep) diff --git a/peco_test.go b/peco_test.go index edef95c..bd7f186 100644 --- a/peco_test.go +++ b/peco_test.go @@ -202,37 +202,10 @@ func TestConfigFuzzyFilter(t *testing.T) { p := newPeco() // Ensure that it's possible to enable the Fuzzy filter - opts.OptFuzzyFilter = "enabled" - if !assert.NoError(t, p.ApplyConfig(opts), "p.ApplyConfig should succeed") { - return - } - if !assert.Equal(t, true, p.enableFuzzy, "p.enableFuzzy should be equal to opts.OptEnableFuzzy") { - return - } - opts.OptFuzzyFilter = "abc" - if !assert.Error(t, p.ApplyConfig(opts), "p.ApplyConfig should not succeed") { - return - } -} - -func TestConfigInitialFilterFuzzy(t *testing.T) { - var opts CLIOptions - p := newPeco() - - // If Fuzzy is not enabled, initialFilter=Fuzzy should cause peco to fail - opts.OptFuzzyFilter = "disabled" opts.OptInitialFilter = "Fuzzy" - if !assert.Error(t, p.ApplyConfig(opts), "p.ApplyConfig should not succeed") { - return - } - // If Fuzzy is enabled, it should be possible to set the initial filter to Fuzzy - opts.OptFuzzyFilter = "enabled" if !assert.NoError(t, p.ApplyConfig(opts), "p.ApplyConfig should succeed") { return } - if !assert.Equal(t, true, p.enableFuzzy, "p.enableFuzzy should be equal to opts.OptEnableFuzzy") { - return - } } func TestApplyConfig(t *testing.T) { From 3476752a12d64e11d9f81cb615115702a24daaf1 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 07:13:10 +0900 Subject: [PATCH 08/14] remove unused fields --- config.go | 1 - config_test.go | 1 - interface.go | 7 ------- 3 files changed, 9 deletions(-) diff --git a/config.go b/config.go index b42428d..586ab35 100644 --- a/config.go +++ b/config.go @@ -25,7 +25,6 @@ func (c *Config) Init() error { c.Style.Init() c.Prompt = "QUERY>" c.Layout = LayoutTypeTopDown - c.FuzzyFilter = OptionDisabled return nil } diff --git a/config_test.go b/config_test.go index f050f63..7c94d0b 100644 --- a/config_test.go +++ b/config_test.go @@ -47,7 +47,6 @@ func TestReadRC(t *testing.T) { InitialMatcher: IgnoreCaseMatch, Layout: DefaultLayoutType, Prompt: "[peco]", - FuzzyFilter: "disabled", Style: StyleSet{ Matched: Style{ fg: termbox.ColorCyan | termbox.AttrBold, diff --git a/interface.go b/interface.go index 37de986..7d56388 100644 --- a/interface.go +++ b/interface.go @@ -56,11 +56,6 @@ type idgen struct { ch chan uint64 } -const ( - OptionEnabled = "enabled" - OptionDisabled = "disabled" -) - // Peco is the global object containing everything required to run peco. // It also contains the global state of the program. type Peco struct { @@ -79,7 +74,6 @@ type Peco struct { enableSep bool // Enable parsing on separators filters FilterSet idgen *idgen - enableFuzzy bool initialFilter string initialQuery string // populated if --query is specified inputseq Inputseq // current key sequence (just the names) @@ -349,7 +343,6 @@ type Config struct { Keymap map[string]string `json:"Keymap"` Matcher string `json:"Matcher"` // Deprecated. InitialMatcher string `json:"InitialMatcher"` // Use this instead of Matcher - FuzzyFilter string `json:"FuzzyFilter"` InitialFilter string `json:"InitialFilter"` Style StyleSet `json:"Style"` Prompt string `json:"Prompt"` From 0ea80d7d90615bdb753b78f55af57a0d8e978f6c Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 07:15:46 +0900 Subject: [PATCH 09/14] remove unused field and params --- filter.go | 15 +++++++-------- filterutil.go | 14 +++----------- interface.go | 11 ----------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/filter.go b/filter.go index a424292..7b16252 100644 --- a/filter.go +++ b/filter.go @@ -202,12 +202,11 @@ func (rf *RegexpFilter) OutCh() <-chan interface{} { func (rf RegexpFilter) Clone() LineFilter { return &RegexpFilter{ - flags: rf.flags, - quotemeta: rf.quotemeta, - query: rf.query, - queryTrans: rf.queryTrans, - name: rf.name, - outCh: pipeline.OutputChannel(make(chan interface{})), + flags: rf.flags, + quotemeta: rf.quotemeta, + query: rf.query, + name: rf.name, + outCh: pipeline.OutputChannel(make(chan interface{})), } } @@ -384,7 +383,7 @@ func (rf *RegexpFilter) getQueryAsRegexps() ([]*regexp.Regexp, error) { if q := rf.compiledQuery; q != nil { return q, nil } - q, err := queryToRegexps(rf.flags, rf.quotemeta, rf.query, rf.queryTrans) + q, err := queryToRegexps(rf.flags, rf.quotemeta, rf.query) if err != nil { return nil, errors.Wrap(err, "failed to compile queries as regular expression") } @@ -505,7 +504,7 @@ func (ff *FuzzyFilter) filter(l Line) (Line, error) { // otherwise we have a match, but the next match must match against // something AFTER the current match txt = txt[i+n:] - matches = append(matches, []int{base + i, base+i+n}) + matches = append(matches, []int{base + i, base + i + n}) base = base + i + n } return NewMatchedLine(l, matches), nil diff --git a/filterutil.go b/filterutil.go index aaa9a48..8082d81 100644 --- a/filterutil.go +++ b/filterutil.go @@ -19,20 +19,12 @@ func (r regexpFlagFunc) flags(s string) []string { return r(s) } -func (t queryTransformerFunc) transform(s string) string { - return t(s) -} - -func regexpFor(q string, flags []string, quotemeta bool, queryTrans queryTransformer) (*regexp.Regexp, error) { +func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) { reTxt := q if quotemeta { reTxt = regexp.QuoteMeta(q) } - if queryTrans != nil { - reTxt = queryTrans.transform(reTxt) - } - if flags != nil && len(flags) > 0 { reTxt = fmt.Sprintf("(?%s)%s", strings.Join(flags, ""), reTxt) } @@ -44,12 +36,12 @@ func regexpFor(q string, flags []string, quotemeta bool, queryTrans queryTransfo return re, nil } -func queryToRegexps(flags regexpFlags, quotemeta bool, query string, queryTrans queryTransformer) ([]*regexp.Regexp, error) { +func queryToRegexps(flags regexpFlags, quotemeta bool, query string) ([]*regexp.Regexp, error) { queries := strings.Split(strings.TrimSpace(query), " ") regexps := make([]*regexp.Regexp, 0) for _, q := range queries { - re, err := regexpFor(q, flags.flags(query), quotemeta, queryTrans) + re, err := regexpFor(q, flags.flags(query), quotemeta) if err != nil { return nil, errors.Wrapf(err, "failed to compile regular expression '%s'", q) } diff --git a/interface.go b/interface.go index 7d56388..069c0d9 100644 --- a/interface.go +++ b/interface.go @@ -297,16 +297,6 @@ type regexpFlagList []string type regexpFlagFunc func(string) []string -// queryTransformer is able to transform a query from one form to another. -// This is used by the FuzzyFilter to transform the user query to a regular -// expression. -type queryTransformer interface { - transform(string) string -} - -// type queryTransformerFunc implements queryTransformer. -type queryTransformerFunc func(string) string - // Filter is responsible for the actual "grep" part of peco type Filter struct { state *Peco @@ -533,7 +523,6 @@ type RegexpFilter struct { flags regexpFlags quotemeta bool query string - queryTrans queryTransformer mutex sync.Mutex name string onEnd func() From 668a498ed8c21360280045e33b0a695a05b67d49 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 07:21:51 +0900 Subject: [PATCH 10/14] slight refactor --- filter.go | 7 ++----- internal/util/util.go | 6 ++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/filter.go b/filter.go index 7b16252..d53407f 100644 --- a/filter.go +++ b/filter.go @@ -9,7 +9,6 @@ import ( "strings" "sync" "time" - "unicode" "unicode/utf8" "github.com/lestrrat/go-pdebug" @@ -479,7 +478,7 @@ func (ff *FuzzyFilter) filter(l Line) (Line, error) { txt := l.DisplayString() matches := [][]int{} - hasUpper := strings.IndexFunc(query, unicode.IsUpper) > -1 + hasUpper := util.ContainsUpper(query) for len(query) > 0 { r, n := utf8.DecodeRuneInString(query) @@ -493,9 +492,7 @@ func (ff *FuzzyFilter) filter(l Line) (Line, error) { if hasUpper { // explicit match i = strings.IndexRune(txt, r) } else { - i = strings.IndexFunc(txt, func(v rune) bool { - return unicode.ToUpper(r) == v || unicode.ToLower(r) == v - }) + i = strings.IndexFunc(txt, util.CaseInsensitiveIndexFunc(r)) } if i == -1 { return nil, errors.New("filter did not match against given line") diff --git a/internal/util/util.go b/internal/util/util.go index 97a24a5..2e5eabe 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -9,6 +9,12 @@ type fder interface { Fd() uintptr } +func CaseInsensitiveIndexFunc(r rune) func(rune) bool { + return func(v rune) bool { + return unicode.ToUpper(r) == v || unicode.ToLower(r) == v + } +} + func ContainsUpper(query string) bool { for _, c := range query { if unicode.IsUpper(c) { From d659c93bdc49bac34b33d8bb74fdcba318da5c45 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 07:24:50 +0900 Subject: [PATCH 11/14] Update changes --- Changes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changes b/Changes index 1e5b5c7..e82f510 100644 --- a/Changes +++ b/Changes @@ -2,6 +2,8 @@ Changes ======= v0.4.6 - Not yet released + Features: + * A new fuzzy filter has now been added. See README for details (#369, #370) Bugs/Fixes * A very subtle timing issue that causes the search to be reset has been resolved (#364) From 32a678602163e95a6e8d6c0ca59eb6ac850bdb9a Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 07:33:57 +0900 Subject: [PATCH 12/14] Update comment --- filter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/filter.go b/filter.go index d53407f..c3b911d 100644 --- a/filter.go +++ b/filter.go @@ -436,8 +436,8 @@ func NewSmartCaseFilter() *RegexpFilter { } // NewFuzzyFilter builds a fuzzy-finder type of filter. -// In effect, this uses a smart case filter, and -// transforms the query from "ABC" to the equivalent of "A(.*)B(.*)C(.*)" +// In effect, this uses a smart case filter, and for q query +// like "ABC" it matches the equivalent of "A(.*)B(.*)C(.*)" func NewFuzzyFilter() *FuzzyFilter { return &FuzzyFilter{} } From 45398301ea7ca50789a0537609efe13cce8752aa Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 10:06:12 +0900 Subject: [PATCH 13/14] Apply mattn's review --- filter.go | 2 +- internal/util/util.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/filter.go b/filter.go index c3b911d..a94f1ff 100644 --- a/filter.go +++ b/filter.go @@ -269,7 +269,7 @@ func filterAcceptAndFilter(ctx context.Context, f filter, in chan interface{}, o go flusher(f, flush, flushDone, out) buf := getFilterLineBuf() - defer func() { releaseFilterLineBuf(buf) }() + defer releaseFilterLineBuf(buf) defer func() { <-flushDone }() // Wait till the flush goroutine is done defer close(flush) // Kill the flush goroutine diff --git a/internal/util/util.go b/internal/util/util.go index 2e5eabe..a54c380 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -10,8 +10,9 @@ type fder interface { } func CaseInsensitiveIndexFunc(r rune) func(rune) bool { + lr := unicode.ToUpper(r) return func(v rune) bool { - return unicode.ToUpper(r) == v || unicode.ToLower(r) == v + return lr == unicode.ToUpper(v) } } From 58b60cd7e319737479e3afd631ba6063498c60d2 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 13 Dec 2016 10:13:10 +0900 Subject: [PATCH 14/14] tweak comments --- filter_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/filter_test.go b/filter_test.go index 6eca22c..4ab2b4c 100644 --- a/filter_test.go +++ b/filter_test.go @@ -21,10 +21,10 @@ func TestFuzzyFilter(t *testing.T) { {"this is a Test to test the fUzzy filter", "TU", true}, // case sensitivity {"this is a test to test the fuzzy filter", "Tu", false}, // case sensitivity {"this is a test to Test the fuzzy filter", "TU", false}, // case sensitivity - {"日本語は難しいです", "難", true}, // kanji - {"あ、日本語は難しいですよ", "あい", true}, // hiragana - {"パソコンは遅いですネ", "ソネ", true}, // katana - {"🚴🏻 abcd efgh", "🚴🏻e", true}, // unicode + {"日本語は難しいです", "難", true}, // kanji + {"あ、日本語は難しいですよ", "あい", true}, // hiragana + {"パソコンは遅いですネ", "ソネ", true}, // katakana + {"🚴🏻 abcd efgh", "🚴🏻e", true}, // unicode } filter := NewFuzzyFilter() for i, v := range testValues {