From 10fd0f857b747e6b00815a6a78da610806d26837 Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 20:16:36 +0900 Subject: [PATCH 01/14] implement searcher and move searchForward and searchBackward --- searcher/searcher.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ window/window.go | 26 +++++------------- 2 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 searcher/searcher.go diff --git a/searcher/searcher.go b/searcher/searcher.go new file mode 100644 index 0000000..627d7a0 --- /dev/null +++ b/searcher/searcher.go @@ -0,0 +1,64 @@ +package searcher + +import ( + "bytes" + "fmt" + "io" + + "github.com/itchyny/bed/mathutil" +) + +// Searcher represents a searcher. +type Searcher struct { + r readAtSeeker +} + +type readAtSeeker interface { + io.ReaderAt + io.Seeker +} + +// NewSearcher creates a new searcher. +func NewSearcher(r readAtSeeker) *Searcher { + return &Searcher{r: r} +} + +// Forward searches the pattern forward. +func (s *Searcher) Forward(cursor int64, pattern string) (int64, error) { + target := []byte(pattern) + base, size := cursor+1, 1000 + _, bs, err := s.readBytes(base, size) + if err != nil { + return -1, err + } + i := bytes.Index(bs, target) + if i >= 0 { + return base + int64(i), nil + } + return -1, fmt.Errorf("pattern not found: %q", pattern) +} + +// Backward searches the pattern backward. +func (s *Searcher) Backward(cursor int64, pattern string) (int64, error) { + target := []byte(pattern) + size := 1000 + base := mathutil.MaxInt64(0, cursor-int64(size)) + _, bs, err := s.readBytes(base, int(mathutil.MinInt64(int64(size), cursor))) + if err != nil { + return -1, err + } + i := bytes.LastIndex(bs, target) + if i >= 0 { + return base + int64(i), nil + } + return -1, fmt.Errorf("pattern not found: %q", pattern) +} + +func (s *Searcher) readBytes(offset int64, len int) (int, []byte, error) { + bytes := make([]byte, len) + n, err := s.r.ReadAt(bytes, offset) + if err != nil && err != io.EOF { + return 0, bytes, err + } + return n, bytes, nil +} diff --git a/window/window.go b/window/window.go index 0050f8f..a0820ba 100644 --- a/window/window.go +++ b/window/window.go @@ -1,7 +1,6 @@ package window import ( - "bytes" "errors" "fmt" "io" @@ -14,6 +13,7 @@ import ( "github.com/itchyny/bed/history" "github.com/itchyny/bed/mathutil" "github.com/itchyny/bed/mode" + "github.com/itchyny/bed/searcher" "github.com/itchyny/bed/state" ) @@ -22,6 +22,7 @@ type window struct { changedTick uint64 prevChanged bool history *history.History + searcher *searcher.Searcher filename string name string height int64 @@ -63,6 +64,7 @@ func newWindow(r readAtSeeker, filename string, name string, eventCh chan<- even return &window{ buffer: buffer, history: history, + searcher: searcher.NewSearcher(r), filename: filename, name: name, length: length, @@ -1003,31 +1005,17 @@ func (w *window) search(str string, forward bool) { } func (w *window) searchForward(str string) { - target := []byte(str) - base, size := w.cursor+1, mathutil.MaxInt(int(w.height*w.width)*50, len(target)*500) - _, bs, err := w.readBytes(base, size) + cursor, err := w.searcher.Forward(w.cursor, str) if err != nil { return } - i := bytes.Index(bs, target) - if i >= 0 { - w.cursor = base + int64(i) - if w.cursor >= w.offset+w.height*w.width { - w.offset = (w.cursor - w.height*w.width + w.width + 1) / w.width * w.width - } - } + w.cursor = cursor } func (w *window) searchBackward(str string) { - target := []byte(str) - size := mathutil.MaxInt(int(w.height*w.width)*50, len(target)*500) - base := mathutil.MaxInt64(0, w.cursor-int64(size)) - _, bs, err := w.readBytes(base, int(mathutil.MinInt64(int64(size), w.cursor))) + cursor, err := w.searcher.Backward(w.cursor, str) if err != nil { return } - i := bytes.LastIndex(bs, target) - if i >= 0 { - w.cursor = base + int64(i) - } + w.cursor = cursor } From 58d8fb6ee93a70abb10105f292f9378b88ef1d1f Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 21:37:28 +0900 Subject: [PATCH 02/14] search pattern asynchronously --- searcher/searcher.go | 84 ++++++++++++++++++++++++++++++++++++-------- window/window.go | 34 ++++++++---------- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index 627d7a0..1fc0c03 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -2,15 +2,19 @@ package searcher import ( "bytes" - "fmt" + "errors" "io" + "time" "github.com/itchyny/bed/mathutil" ) // Searcher represents a searcher. type Searcher struct { - r readAtSeeker + r readAtSeeker + loopCh chan struct{} + cursor int64 + pattern string } type readAtSeeker interface { @@ -23,35 +27,87 @@ func NewSearcher(r readAtSeeker) *Searcher { return &Searcher{r: r} } +var errNotFound = errors.New("pattern not found") + +const loadSize = 1024 * 1024 + // Forward searches the pattern forward. -func (s *Searcher) Forward(cursor int64, pattern string) (int64, error) { - target := []byte(pattern) - base, size := cursor+1, 1000 - _, bs, err := s.readBytes(base, size) +func (s *Searcher) Forward(cursor int64, pattern string) <-chan int64 { + s.cursor, s.pattern = cursor, pattern + ch := make(chan int64) + s.loop(s.forward, ch) + return ch +} + +func (s *Searcher) forward() (int64, error) { + target := []byte(s.pattern) + base := s.cursor + 1 + n, bs, err := s.readBytes(base, loadSize) if err != nil { return -1, err } + if n == 0 { + return -1, errNotFound + } + s.cursor += int64(n) i := bytes.Index(bs, target) if i >= 0 { return base + int64(i), nil } - return -1, fmt.Errorf("pattern not found: %q", pattern) + return -1, nil } -// Backward searches the pattern backward. -func (s *Searcher) Backward(cursor int64, pattern string) (int64, error) { - target := []byte(pattern) - size := 1000 - base := mathutil.MaxInt64(0, cursor-int64(size)) - _, bs, err := s.readBytes(base, int(mathutil.MinInt64(int64(size), cursor))) +// Backward searches the pattern forward. +func (s *Searcher) Backward(cursor int64, pattern string) <-chan int64 { + s.cursor, s.pattern = cursor, pattern + ch := make(chan int64) + s.loop(s.backward, ch) + return ch +} + +func (s *Searcher) backward() (int64, error) { + target := []byte(s.pattern) + base := mathutil.MaxInt64(0, s.cursor-int64(loadSize)) + n, bs, err := s.readBytes(base, int(mathutil.MinInt64(int64(loadSize), s.cursor))) if err != nil { return -1, err } + if n == 0 { + return -1, errNotFound + } + s.cursor = base i := bytes.LastIndex(bs, target) if i >= 0 { return base + int64(i), nil } - return -1, fmt.Errorf("pattern not found: %q", pattern) + return -1, nil +} + +func (s *Searcher) loop(f func() (int64, error), ch chan<- int64) { + if s.loopCh != nil { + close(s.loopCh) + } + s.loopCh = make(chan struct{}) + go func() { + for { + select { + case <-s.loopCh: + return + case <-time.After(10 * time.Millisecond): + idx, err := f() + if err != nil { + ch <- -1 + close(ch) + return + } + if idx >= 0 { + ch <- idx + close(ch) + return + } + } + } + }() } func (s *Searcher) readBytes(offset int64, len int) (int, []byte, error) { diff --git a/window/window.go b/window/window.go index a0820ba..8d93e7b 100644 --- a/window/window.go +++ b/window/window.go @@ -997,25 +997,21 @@ func (w *window) paste(e event.Event) int64 { } func (w *window) search(str string, forward bool) { + var ch <-chan int64 if forward { - w.searchForward(str) + ch = w.searcher.Forward(w.cursor, str) } else { - w.searchBackward(str) - } -} - -func (w *window) searchForward(str string) { - cursor, err := w.searcher.Forward(w.cursor, str) - if err != nil { - return - } - w.cursor = cursor -} - -func (w *window) searchBackward(str string) { - cursor, err := w.searcher.Backward(w.cursor, str) - if err != nil { - return - } - w.cursor = cursor + ch = w.searcher.Backward(w.cursor, str) + } + go func() { + select { + case cursor := <-ch: + if cursor >= 0 { + w.mu.Lock() + w.cursor = cursor + w.mu.Unlock() + w.redrawCh <- struct{}{} + } + } + }() } From e9d38a47e40d1e46dc00151ea0083c102a1a14ad Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 21:42:31 +0900 Subject: [PATCH 03/14] add mutex to searcher --- searcher/searcher.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index 1fc0c03..205b9b2 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "io" + "sync" "time" "github.com/itchyny/bed/mathutil" @@ -15,6 +16,7 @@ type Searcher struct { loopCh chan struct{} cursor int64 pattern string + mu *sync.Mutex } type readAtSeeker interface { @@ -24,7 +26,7 @@ type readAtSeeker interface { // NewSearcher creates a new searcher. func NewSearcher(r readAtSeeker) *Searcher { - return &Searcher{r: r} + return &Searcher{r: r, mu: new(sync.Mutex)} } var errNotFound = errors.New("pattern not found") @@ -33,6 +35,8 @@ const loadSize = 1024 * 1024 // Forward searches the pattern forward. func (s *Searcher) Forward(cursor int64, pattern string) <-chan int64 { + s.mu.Lock() + defer s.mu.Unlock() s.cursor, s.pattern = cursor, pattern ch := make(chan int64) s.loop(s.forward, ch) @@ -40,6 +44,8 @@ func (s *Searcher) Forward(cursor int64, pattern string) <-chan int64 { } func (s *Searcher) forward() (int64, error) { + s.mu.Lock() + defer s.mu.Unlock() target := []byte(s.pattern) base := s.cursor + 1 n, bs, err := s.readBytes(base, loadSize) @@ -59,6 +65,8 @@ func (s *Searcher) forward() (int64, error) { // Backward searches the pattern forward. func (s *Searcher) Backward(cursor int64, pattern string) <-chan int64 { + s.mu.Lock() + defer s.mu.Unlock() s.cursor, s.pattern = cursor, pattern ch := make(chan int64) s.loop(s.backward, ch) @@ -66,6 +74,8 @@ func (s *Searcher) Backward(cursor int64, pattern string) <-chan int64 { } func (s *Searcher) backward() (int64, error) { + s.mu.Lock() + defer s.mu.Unlock() target := []byte(s.pattern) base := mathutil.MaxInt64(0, s.cursor-int64(loadSize)) n, bs, err := s.readBytes(base, int(mathutil.MinInt64(int64(loadSize), s.cursor))) From 194545a463a47ecc1322cbf665ba2992cb75bf83 Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 21:53:49 +0900 Subject: [PATCH 04/14] print error when search pattern is not found --- editor/editor.go | 4 ++-- searcher/searcher.go | 26 ++++++++++++++------------ window/window.go | 11 +++++++---- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/editor/editor.go b/editor/editor.go index 32f2e33..cb58145 100644 --- a/editor/editor.go +++ b/editor/editor.go @@ -205,9 +205,9 @@ func (e *Editor) emit(ev event.Event) (redraw bool, finish bool) { case event.ExecuteSearch: e.searchTarget, e.searchMode = ev.Arg, ev.Rune case event.NextSearch: - ev.Arg, ev.Rune = e.searchTarget, e.searchMode + ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil case event.PreviousSearch: - ev.Arg, ev.Rune = e.searchTarget, e.searchMode + ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil case event.Paste, event.PastePrev: if e.buffer == nil { e.mu.Unlock() diff --git a/searcher/searcher.go b/searcher/searcher.go index 205b9b2..7ebfed1 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -2,7 +2,6 @@ package searcher import ( "bytes" - "errors" "io" "sync" "time" @@ -29,16 +28,20 @@ func NewSearcher(r readAtSeeker) *Searcher { return &Searcher{r: r, mu: new(sync.Mutex)} } -var errNotFound = errors.New("pattern not found") +type errNotFound string + +func (err errNotFound) Error() string { + return "pattern not found: " + string(err) +} const loadSize = 1024 * 1024 // Forward searches the pattern forward. -func (s *Searcher) Forward(cursor int64, pattern string) <-chan int64 { +func (s *Searcher) Forward(cursor int64, pattern string) <-chan interface{} { s.mu.Lock() defer s.mu.Unlock() s.cursor, s.pattern = cursor, pattern - ch := make(chan int64) + ch := make(chan interface{}) s.loop(s.forward, ch) return ch } @@ -53,7 +56,7 @@ func (s *Searcher) forward() (int64, error) { return -1, err } if n == 0 { - return -1, errNotFound + return -1, errNotFound(s.pattern) } s.cursor += int64(n) i := bytes.Index(bs, target) @@ -64,11 +67,11 @@ func (s *Searcher) forward() (int64, error) { } // Backward searches the pattern forward. -func (s *Searcher) Backward(cursor int64, pattern string) <-chan int64 { +func (s *Searcher) Backward(cursor int64, pattern string) <-chan interface{} { s.mu.Lock() defer s.mu.Unlock() s.cursor, s.pattern = cursor, pattern - ch := make(chan int64) + ch := make(chan interface{}) s.loop(s.backward, ch) return ch } @@ -83,7 +86,7 @@ func (s *Searcher) backward() (int64, error) { return -1, err } if n == 0 { - return -1, errNotFound + return -1, errNotFound(s.pattern) } s.cursor = base i := bytes.LastIndex(bs, target) @@ -93,12 +96,13 @@ func (s *Searcher) backward() (int64, error) { return -1, nil } -func (s *Searcher) loop(f func() (int64, error), ch chan<- int64) { +func (s *Searcher) loop(f func() (int64, error), ch chan<- interface{}) { if s.loopCh != nil { close(s.loopCh) } s.loopCh = make(chan struct{}) go func() { + defer close(ch) for { select { case <-s.loopCh: @@ -106,13 +110,11 @@ func (s *Searcher) loop(f func() (int64, error), ch chan<- int64) { case <-time.After(10 * time.Millisecond): idx, err := f() if err != nil { - ch <- -1 - close(ch) + ch <- err return } if idx >= 0 { ch <- idx - close(ch) return } } diff --git a/window/window.go b/window/window.go index 8d93e7b..2cc7907 100644 --- a/window/window.go +++ b/window/window.go @@ -997,7 +997,7 @@ func (w *window) paste(e event.Event) int64 { } func (w *window) search(str string, forward bool) { - var ch <-chan int64 + var ch <-chan interface{} if forward { ch = w.searcher.Forward(w.cursor, str) } else { @@ -1005,10 +1005,13 @@ func (w *window) search(str string, forward bool) { } go func() { select { - case cursor := <-ch: - if cursor >= 0 { + case x := <-ch: + switch x := x.(type) { + case error: + w.eventCh <- event.Event{Type: event.Info, Error: x} + case int64: w.mu.Lock() - w.cursor = cursor + w.cursor = x w.mu.Unlock() w.redrawCh <- struct{}{} } From 6d0c84ad01bf54a2f686ee103ee5f6fb11eb8f38 Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 22:05:31 +0900 Subject: [PATCH 05/14] fix data race of searcher loop channel --- searcher/searcher.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index 7ebfed1..c00bedb 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -100,12 +100,13 @@ func (s *Searcher) loop(f func() (int64, error), ch chan<- interface{}) { if s.loopCh != nil { close(s.loopCh) } - s.loopCh = make(chan struct{}) + loopCh := make(chan struct{}) + s.loopCh = loopCh go func() { defer close(ch) for { select { - case <-s.loopCh: + case <-loopCh: return case <-time.After(10 * time.Millisecond): idx, err := f() From 4d001145934e7ae879506561040413c46d8da92f Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 22:34:58 +0900 Subject: [PATCH 06/14] merge search forward and backward implementation --- searcher/searcher.go | 20 +++++++------------- window/window.go | 7 +------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index c00bedb..ac15a20 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -36,13 +36,17 @@ func (err errNotFound) Error() string { const loadSize = 1024 * 1024 -// Forward searches the pattern forward. -func (s *Searcher) Forward(cursor int64, pattern string) <-chan interface{} { +// Search the pattern. +func (s *Searcher) Search(cursor int64, pattern string, forward bool) <-chan interface{} { s.mu.Lock() defer s.mu.Unlock() s.cursor, s.pattern = cursor, pattern ch := make(chan interface{}) - s.loop(s.forward, ch) + if forward { + s.loop(s.forward, ch) + } else { + s.loop(s.backward, ch) + } return ch } @@ -66,16 +70,6 @@ func (s *Searcher) forward() (int64, error) { return -1, nil } -// Backward searches the pattern forward. -func (s *Searcher) Backward(cursor int64, pattern string) <-chan interface{} { - s.mu.Lock() - defer s.mu.Unlock() - s.cursor, s.pattern = cursor, pattern - ch := make(chan interface{}) - s.loop(s.backward, ch) - return ch -} - func (s *Searcher) backward() (int64, error) { s.mu.Lock() defer s.mu.Unlock() diff --git a/window/window.go b/window/window.go index 2cc7907..f95ad84 100644 --- a/window/window.go +++ b/window/window.go @@ -997,12 +997,7 @@ func (w *window) paste(e event.Event) int64 { } func (w *window) search(str string, forward bool) { - var ch <-chan interface{} - if forward { - ch = w.searcher.Forward(w.cursor, str) - } else { - ch = w.searcher.Backward(w.cursor, str) - } + ch := w.searcher.Search(w.cursor, str, forward) go func() { select { case x := <-ch: From dc02e89725a0b510c16ea02e8ba9c650e00b1438 Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 22:35:15 +0900 Subject: [PATCH 07/14] add tests for searcher --- searcher/searcher_test.go | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 searcher/searcher_test.go diff --git a/searcher/searcher_test.go b/searcher/searcher_test.go new file mode 100644 index 0000000..ef58d65 --- /dev/null +++ b/searcher/searcher_test.go @@ -0,0 +1,79 @@ +package searcher + +import ( + "strings" + "testing" +) + +func TestSearcher(t *testing.T) { + testCases := []struct { + str string + cursor int64 + pattern string + forward bool + expected int64 + err error + }{ + { + str: "abcde", + cursor: 0, + pattern: "cd", + forward: true, + expected: 2, + }, + { + str: "abcde", + cursor: 2, + pattern: "cd", + forward: true, + err: errNotFound("cd"), + }, + { + str: "abcde", + cursor: 4, + pattern: "bc", + forward: false, + expected: 1, + }, + { + str: "abcde", + cursor: 0, + pattern: "ba", + forward: true, + err: errNotFound("ba"), + }, + { + str: strings.Repeat(" ", 10*1024*1024+100) + "abcde", + cursor: 0, + pattern: "bcd", + forward: true, + expected: 10*1024*1024 + 101, + }, + { + str: "abcde" + strings.Repeat(" ", 10*1024*1024), + cursor: 10 * 1024 * 1024, + pattern: "bcd", + forward: false, + expected: 1, + }, + } + for _, testCase := range testCases { + s := NewSearcher(strings.NewReader(testCase.str)) + ch := s.Search(testCase.cursor, testCase.pattern, testCase.forward) + select { + case x := <-ch: + switch x := x.(type) { + case error: + if testCase.err == nil { + t.Error(x) + } else if x != testCase.err { + t.Errorf("Error should be %v but got %v", testCase.err, x) + } + case int64: + if x != testCase.expected { + t.Errorf("Search result should be %d but got %d", testCase.expected, x) + } + } + } + } +} From ce8ac0b0ad95d69bf2756d065e589dce1536332e Mon Sep 17 00:00:00 2001 From: itchyny Date: Mon, 21 Jan 2019 23:06:45 +0900 Subject: [PATCH 08/14] remove io.Seeker from reader constraint of searcher --- searcher/searcher.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index ac15a20..071dc14 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -11,20 +11,15 @@ import ( // Searcher represents a searcher. type Searcher struct { - r readAtSeeker + r io.ReaderAt loopCh chan struct{} cursor int64 pattern string mu *sync.Mutex } -type readAtSeeker interface { - io.ReaderAt - io.Seeker -} - // NewSearcher creates a new searcher. -func NewSearcher(r readAtSeeker) *Searcher { +func NewSearcher(r io.ReaderAt) *Searcher { return &Searcher{r: r, mu: new(sync.Mutex)} } From c1acdf8589a5e6364f05c205e42ea744e97ac117 Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 01:05:11 +0900 Subject: [PATCH 09/14] fix searcher not to miss the pattern --- searcher/searcher.go | 24 +++++++++++++++--------- searcher/searcher_test.go | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index 071dc14..25378a9 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -51,13 +51,17 @@ func (s *Searcher) forward() (int64, error) { target := []byte(s.pattern) base := s.cursor + 1 n, bs, err := s.readBytes(base, loadSize) - if err != nil { + if err != nil && err != io.EOF { return -1, err } if n == 0 { return -1, errNotFound(s.pattern) } - s.cursor += int64(n) + if err == io.EOF { + s.cursor += int64(n) + } else { + s.cursor += int64(n - len(target) + 1) + } i := bytes.Index(bs, target) if i >= 0 { return base + int64(i), nil @@ -70,14 +74,19 @@ func (s *Searcher) backward() (int64, error) { defer s.mu.Unlock() target := []byte(s.pattern) base := mathutil.MaxInt64(0, s.cursor-int64(loadSize)) - n, bs, err := s.readBytes(base, int(mathutil.MinInt64(int64(loadSize), s.cursor))) - if err != nil { + size := int(mathutil.MinInt64(int64(loadSize), s.cursor)) + n, bs, err := s.readBytes(base, size) + if err != nil && err != io.EOF { return -1, err } if n == 0 { return -1, errNotFound(s.pattern) } - s.cursor = base + if s.cursor == int64(n) { + s.cursor = 0 + } else { + s.cursor = base + int64(len(target)-1) + } i := bytes.LastIndex(bs, target) if i >= 0 { return base + int64(i), nil @@ -115,8 +124,5 @@ func (s *Searcher) loop(f func() (int64, error), ch chan<- interface{}) { func (s *Searcher) readBytes(offset int64, len int) (int, []byte, error) { bytes := make([]byte, len) n, err := s.r.ReadAt(bytes, offset) - if err != nil && err != io.EOF { - return 0, bytes, err - } - return n, bytes, nil + return n, bytes, err } diff --git a/searcher/searcher_test.go b/searcher/searcher_test.go index ef58d65..a0871b3 100644 --- a/searcher/searcher_test.go +++ b/searcher/searcher_test.go @@ -44,18 +44,32 @@ func TestSearcher(t *testing.T) { }, { str: strings.Repeat(" ", 10*1024*1024+100) + "abcde", - cursor: 0, + cursor: 102, pattern: "bcd", forward: true, expected: 10*1024*1024 + 101, }, + { + str: strings.Repeat(" ", 10*1024*1024+100) + "abcde", + cursor: 102, + pattern: "cba", + forward: true, + err: errNotFound("cba"), + }, { str: "abcde" + strings.Repeat(" ", 10*1024*1024), - cursor: 10 * 1024 * 1024, + cursor: 10*1024*1024 + 2, pattern: "bcd", forward: false, expected: 1, }, + { + str: "abcde" + strings.Repeat(" ", 10*1024*1024), + cursor: 10*1024*1024 + 2, + pattern: "cba", + forward: false, + err: errNotFound("cba"), + }, } for _, testCase := range testCases { s := NewSearcher(strings.NewReader(testCase.str)) From e31a3cbc326cc3793ae7288a3835706dba86d85d Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 01:36:29 +0900 Subject: [PATCH 10/14] implement aborting the current search --- editor/key.go | 1 + event/event.go | 1 + searcher/searcher.go | 13 +++++++++++++ window/window.go | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/editor/key.go b/editor/key.go index 1c60bdd..61134bf 100644 --- a/editor/key.go +++ b/editor/key.go @@ -183,6 +183,7 @@ func defaultNormalAndVisual() *key.Manager { km.Register(event.StartCmdlineSearchBackward, "?") km.Register(event.NextSearch, "n") km.Register(event.PreviousSearch, "N") + km.Register(event.AbortSearch, "c-c") km.Register(event.StartCmdlineCommand, ":") km.Register(event.StartReplaceByte, "r") diff --git a/event/event.go b/event/event.go index 4da3a5f..7b3ed30 100644 --- a/event/event.go +++ b/event/event.go @@ -106,6 +106,7 @@ const ( ExecuteSearch NextSearch PreviousSearch + AbortSearch Edit Enew diff --git a/searcher/searcher.go b/searcher/searcher.go index 25378a9..a30b960 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -2,6 +2,7 @@ package searcher import ( "bytes" + "errors" "io" "sync" "time" @@ -126,3 +127,15 @@ func (s *Searcher) readBytes(offset int64, len int) (int, []byte, error) { n, err := s.r.ReadAt(bytes, offset) return n, bytes, err } + +// Abort the searching. +func (s *Searcher) Abort() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.loopCh != nil { + close(s.loopCh) + s.loopCh = nil + return errors.New("search is aborted") + } + return nil +} diff --git a/window/window.go b/window/window.go index f95ad84..4aea69e 100644 --- a/window/window.go +++ b/window/window.go @@ -229,6 +229,8 @@ func (w *window) emit(e event.Event) { w.search(e.Arg, e.Rune == '/') case event.PreviousSearch: w.search(e.Arg, e.Rune != '/') + case event.AbortSearch: + w.abortSearch() default: w.mu.Unlock() return @@ -1013,3 +1015,9 @@ func (w *window) search(str string, forward bool) { } }() } + +func (w *window) abortSearch() { + if err := w.searcher.Abort(); err != nil { + w.eventCh <- event.Event{Type: event.Info, Error: err} + } +} From 4b8cdb1baf54122d647c811c2192e8466315a370 Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 02:04:20 +0900 Subject: [PATCH 11/14] update searcher after editting the buffer --- window/window.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/window/window.go b/window/window.go index 4aea69e..c99aebe 100644 --- a/window/window.go +++ b/window/window.go @@ -23,6 +23,7 @@ type window struct { prevChanged bool history *history.History searcher *searcher.Searcher + searchTick uint64 filename string name string height int64 @@ -999,6 +1000,11 @@ func (w *window) paste(e event.Event) int64 { } func (w *window) search(str string, forward bool) { + if w.searchTick != w.changedTick { + w.searcher.Abort() + w.searcher = searcher.NewSearcher(w.buffer) + w.searchTick = w.changedTick + } ch := w.searcher.Search(w.cursor, str, forward) go func() { select { From 94b89f90a2d9c96730c4caa3f21c2bf46e5afe3c Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 02:04:30 +0900 Subject: [PATCH 12/14] update changedTick on undo and redo --- window/window.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/window/window.go b/window/window.go index c99aebe..4087863 100644 --- a/window/window.go +++ b/window/window.go @@ -386,6 +386,7 @@ func (w *window) undo(count int64) { w.buffer, w.offset, w.cursor = buffer, offset, cursor w.length, _ = w.buffer.Len() } + w.changedTick++ } func (w *window) redo(count int64) { @@ -397,6 +398,7 @@ func (w *window) redo(count int64) { w.buffer, w.offset, w.cursor = buffer, offset, cursor w.length, _ = w.buffer.Len() } + w.changedTick++ } func (w *window) cursorUp(count int64) { From f27d23d7fe89fc89bcec5afb7c2c379f82fa8baf Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 09:31:00 +0900 Subject: [PATCH 13/14] reuse the byte slice in searcher to reduce memory footprint --- searcher/searcher.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/searcher/searcher.go b/searcher/searcher.go index a30b960..ed63720 100644 --- a/searcher/searcher.go +++ b/searcher/searcher.go @@ -10,9 +10,12 @@ import ( "github.com/itchyny/bed/mathutil" ) +const loadSize = 1024 * 1024 + // Searcher represents a searcher. type Searcher struct { r io.ReaderAt + bytes []byte loopCh chan struct{} cursor int64 pattern string @@ -21,7 +24,7 @@ type Searcher struct { // NewSearcher creates a new searcher. func NewSearcher(r io.ReaderAt) *Searcher { - return &Searcher{r: r, mu: new(sync.Mutex)} + return &Searcher{r: r, bytes: make([]byte, loadSize), mu: new(sync.Mutex)} } type errNotFound string @@ -30,8 +33,6 @@ func (err errNotFound) Error() string { return "pattern not found: " + string(err) } -const loadSize = 1024 * 1024 - // Search the pattern. func (s *Searcher) Search(cursor int64, pattern string, forward bool) <-chan interface{} { s.mu.Lock() @@ -51,7 +52,7 @@ func (s *Searcher) forward() (int64, error) { defer s.mu.Unlock() target := []byte(s.pattern) base := s.cursor + 1 - n, bs, err := s.readBytes(base, loadSize) + n, err := s.r.ReadAt(s.bytes, base) if err != nil && err != io.EOF { return -1, err } @@ -63,7 +64,7 @@ func (s *Searcher) forward() (int64, error) { } else { s.cursor += int64(n - len(target) + 1) } - i := bytes.Index(bs, target) + i := bytes.Index(s.bytes[:n], target) if i >= 0 { return base + int64(i), nil } @@ -76,7 +77,7 @@ func (s *Searcher) backward() (int64, error) { target := []byte(s.pattern) base := mathutil.MaxInt64(0, s.cursor-int64(loadSize)) size := int(mathutil.MinInt64(int64(loadSize), s.cursor)) - n, bs, err := s.readBytes(base, size) + n, err := s.r.ReadAt(s.bytes[:size], base) if err != nil && err != io.EOF { return -1, err } @@ -88,7 +89,7 @@ func (s *Searcher) backward() (int64, error) { } else { s.cursor = base + int64(len(target)-1) } - i := bytes.LastIndex(bs, target) + i := bytes.LastIndex(s.bytes[:n], target) if i >= 0 { return base + int64(i), nil } @@ -122,12 +123,6 @@ func (s *Searcher) loop(f func() (int64, error), ch chan<- interface{}) { }() } -func (s *Searcher) readBytes(offset int64, len int) (int, []byte, error) { - bytes := make([]byte, len) - n, err := s.r.ReadAt(bytes, offset) - return n, bytes, err -} - // Abort the searching. func (s *Searcher) Abort() error { s.mu.Lock() From f7b70719903cf5dc09d3fd110d2aa25dd9dac03e Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 22 Jan 2019 10:32:18 +0900 Subject: [PATCH 14/14] add test case names in tests for searcher --- searcher/searcher_test.go | 41 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/searcher/searcher_test.go b/searcher/searcher_test.go index a0871b3..3e4861c 100644 --- a/searcher/searcher_test.go +++ b/searcher/searcher_test.go @@ -7,6 +7,7 @@ import ( func TestSearcher(t *testing.T) { testCases := []struct { + name string str string cursor int64 pattern string @@ -15,6 +16,7 @@ func TestSearcher(t *testing.T) { err error }{ { + name: "search forward", str: "abcde", cursor: 0, pattern: "cd", @@ -22,6 +24,7 @@ func TestSearcher(t *testing.T) { expected: 2, }, { + name: "search forward but not found", str: "abcde", cursor: 2, pattern: "cd", @@ -29,6 +32,7 @@ func TestSearcher(t *testing.T) { err: errNotFound("cd"), }, { + name: "search backward", str: "abcde", cursor: 4, pattern: "bc", @@ -36,6 +40,7 @@ func TestSearcher(t *testing.T) { expected: 1, }, { + name: "search backward but not found", str: "abcde", cursor: 0, pattern: "ba", @@ -43,6 +48,7 @@ func TestSearcher(t *testing.T) { err: errNotFound("ba"), }, { + name: "search large target forward", str: strings.Repeat(" ", 10*1024*1024+100) + "abcde", cursor: 102, pattern: "bcd", @@ -50,6 +56,7 @@ func TestSearcher(t *testing.T) { expected: 10*1024*1024 + 101, }, { + name: "search large target forward but not found", str: strings.Repeat(" ", 10*1024*1024+100) + "abcde", cursor: 102, pattern: "cba", @@ -57,6 +64,7 @@ func TestSearcher(t *testing.T) { err: errNotFound("cba"), }, { + name: "search large target backward", str: "abcde" + strings.Repeat(" ", 10*1024*1024), cursor: 10*1024*1024 + 2, pattern: "bcd", @@ -64,6 +72,7 @@ func TestSearcher(t *testing.T) { expected: 1, }, { + name: "search large target backward but not found", str: "abcde" + strings.Repeat(" ", 10*1024*1024), cursor: 10*1024*1024 + 2, pattern: "cba", @@ -72,22 +81,24 @@ func TestSearcher(t *testing.T) { }, } for _, testCase := range testCases { - s := NewSearcher(strings.NewReader(testCase.str)) - ch := s.Search(testCase.cursor, testCase.pattern, testCase.forward) - select { - case x := <-ch: - switch x := x.(type) { - case error: - if testCase.err == nil { - t.Error(x) - } else if x != testCase.err { - t.Errorf("Error should be %v but got %v", testCase.err, x) - } - case int64: - if x != testCase.expected { - t.Errorf("Search result should be %d but got %d", testCase.expected, x) + t.Run(testCase.name, func(t *testing.T) { + s := NewSearcher(strings.NewReader(testCase.str)) + ch := s.Search(testCase.cursor, testCase.pattern, testCase.forward) + select { + case x := <-ch: + switch x := x.(type) { + case error: + if testCase.err == nil { + t.Error(x) + } else if x != testCase.err { + t.Errorf("Error should be %v but got %v", testCase.err, x) + } + case int64: + if x != testCase.expected { + t.Errorf("Search result should be %d but got %d", testCase.expected, x) + } } } - } + }) } }