Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement asynchronous searcher #23

Merged
merged 14 commits into from
Jan 22, 2019
Merged
4 changes: 2 additions & 2 deletions editor/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions editor/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const (
ExecuteSearch
NextSearch
PreviousSearch
AbortSearch

Edit
Enew
Expand Down
136 changes: 136 additions & 0 deletions searcher/searcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package searcher

import (
"bytes"
"errors"
"io"
"sync"
"time"

"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
mu *sync.Mutex
}

// NewSearcher creates a new searcher.
func NewSearcher(r io.ReaderAt) *Searcher {
return &Searcher{r: r, bytes: make([]byte, loadSize), mu: new(sync.Mutex)}
}

type errNotFound string

func (err errNotFound) Error() string {
return "pattern not found: " + string(err)
}

// 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{})
if forward {
s.loop(s.forward, ch)
} else {
s.loop(s.backward, ch)
}
return ch
}

func (s *Searcher) forward() (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
target := []byte(s.pattern)
base := s.cursor + 1
n, err := s.r.ReadAt(s.bytes, base)
if err != nil && err != io.EOF {
return -1, err
}
if n == 0 {
return -1, errNotFound(s.pattern)
}
if err == io.EOF {
s.cursor += int64(n)
} else {
s.cursor += int64(n - len(target) + 1)
}
i := bytes.Index(s.bytes[:n], target)
if i >= 0 {
return base + int64(i), nil
}
return -1, nil
}

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))
size := int(mathutil.MinInt64(int64(loadSize), s.cursor))
n, err := s.r.ReadAt(s.bytes[:size], base)
if err != nil && err != io.EOF {
return -1, err
}
if n == 0 {
return -1, errNotFound(s.pattern)
}
if s.cursor == int64(n) {
s.cursor = 0
} else {
s.cursor = base + int64(len(target)-1)
}
i := bytes.LastIndex(s.bytes[:n], target)
if i >= 0 {
return base + int64(i), nil
}
return -1, nil
}

func (s *Searcher) loop(f func() (int64, error), ch chan<- interface{}) {
if s.loopCh != nil {
close(s.loopCh)
}
loopCh := make(chan struct{})
s.loopCh = loopCh
go func() {
defer close(ch)
for {
select {
case <-loopCh:
return
case <-time.After(10 * time.Millisecond):
idx, err := f()
if err != nil {
ch <- err
return
}
if idx >= 0 {
ch <- idx
return
}
}
}
}()
}

// 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
}
104 changes: 104 additions & 0 deletions searcher/searcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package searcher

import (
"strings"
"testing"
)

func TestSearcher(t *testing.T) {
testCases := []struct {
name string
str string
cursor int64
pattern string
forward bool
expected int64
err error
}{
{
name: "search forward",
str: "abcde",
cursor: 0,
pattern: "cd",
forward: true,
expected: 2,
},
{
name: "search forward but not found",
str: "abcde",
cursor: 2,
pattern: "cd",
forward: true,
err: errNotFound("cd"),
},
{
name: "search backward",
str: "abcde",
cursor: 4,
pattern: "bc",
forward: false,
expected: 1,
},
{
name: "search backward but not found",
str: "abcde",
cursor: 0,
pattern: "ba",
forward: true,
err: errNotFound("ba"),
},
{
name: "search large target forward",
str: strings.Repeat(" ", 10*1024*1024+100) + "abcde",
cursor: 102,
pattern: "bcd",
forward: true,
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",
forward: true,
err: errNotFound("cba"),
},
{
name: "search large target backward",
str: "abcde" + strings.Repeat(" ", 10*1024*1024),
cursor: 10*1024*1024 + 2,
pattern: "bcd",
forward: false,
expected: 1,
},
{
name: "search large target backward but not found",
str: "abcde" + strings.Repeat(" ", 10*1024*1024),
cursor: 10*1024*1024 + 2,
pattern: "cba",
forward: false,
err: errNotFound("cba"),
},
}
for _, testCase := range testCases {
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)
}
}
}
})
}
}
62 changes: 30 additions & 32 deletions window/window.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package window

import (
"bytes"
"errors"
"fmt"
"io"
Expand All @@ -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"
)

Expand All @@ -22,6 +22,8 @@ type window struct {
changedTick uint64
prevChanged bool
history *history.History
searcher *searcher.Searcher
searchTick uint64
filename string
name string
height int64
Expand Down Expand Up @@ -63,6 +65,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,
Expand Down Expand Up @@ -227,6 +230,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
Expand Down Expand Up @@ -381,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) {
Expand All @@ -392,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) {
Expand Down Expand Up @@ -995,39 +1002,30 @@ func (w *window) paste(e event.Event) int64 {
}

func (w *window) search(str string, forward bool) {
if forward {
w.searchForward(str)
} else {
w.searchBackward(str)
}
}

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)
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
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 {
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 = x
w.mu.Unlock()
w.redrawCh <- struct{}{}
}
}
}
}()
}

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)))
if err != nil {
return
}
i := bytes.LastIndex(bs, target)
if i >= 0 {
w.cursor = base + int64(i)
func (w *window) abortSearch() {
if err := w.searcher.Abort(); err != nil {
w.eventCh <- event.Event{Type: event.Info, Error: err}
}
}