package window import ( "cmp" "errors" "fmt" "io" "strconv" "sync" "unicode" "unicode/utf8" "b612.me/apps/b612/bed/buffer" "b612.me/apps/b612/bed/event" "b612.me/apps/b612/bed/history" "b612.me/apps/b612/bed/mode" "b612.me/apps/b612/bed/searcher" "b612.me/apps/b612/bed/state" ) type window struct { buffer *buffer.Buffer changedTick uint64 prevChanged bool maxChangedTick uint64 savedChangedTick uint64 history *history.History searcher *searcher.Searcher searchTick uint64 path string name string height int64 width int64 offset int64 cursor int64 length int64 stack []position append bool replaceByte bool extending bool pending bool pendingByte byte visualStart int64 focusText bool buf []byte buf1 [1]byte redrawCh chan<- struct{} eventCh chan<- event.Event mu *sync.Mutex } type position struct { cursor int64 offset int64 } type readAtSeeker interface { io.ReaderAt io.Seeker } func newWindow( r readAtSeeker, path, name string, eventCh chan<- event.Event, redrawCh chan<- struct{}, ) (*window, error) { buffer := buffer.NewBuffer(r) length, err := buffer.Len() if err != nil { return nil, err } history := history.NewHistory() history.Push(buffer, 0, 0, 0) return &window{ buffer: buffer, history: history, searcher: searcher.NewSearcher(r), path: path, name: name, length: length, visualStart: -1, redrawCh: redrawCh, eventCh: eventCh, mu: new(sync.Mutex), }, nil } func (w *window) setSize(width, height int) { w.width, w.height = int64(width), int64(height) w.offset = w.offset / w.width * w.width if w.cursor < w.offset { w.offset = w.cursor / w.width * w.width } else if w.cursor >= w.offset+w.height*w.width { w.offset = (w.cursor - w.height*w.width + w.width) / w.width * w.width } w.offset = min( w.offset, max(w.length-1-w.height*w.width+w.width, 0)/w.width*w.width, ) } func (w *window) emit(e event.Event) { var newEvent event.Event w.mu.Lock() offset, cursor, changedTick := w.offset, w.cursor, w.changedTick switch e.Type { case event.CursorUp: w.cursorUp(e.Count) case event.CursorDown: w.cursorDown(e.Count) case event.CursorLeft: w.cursorLeft(e.Count) case event.CursorRight: w.cursorRight(e.Mode, e.Count) case event.CursorPrev: w.cursorPrev(e.Count) case event.CursorNext: w.cursorNext(e.Mode, e.Count) case event.CursorHead: w.cursorHead(e.Count) case event.CursorEnd: w.cursorEnd(e.Count) case event.CursorGoto: w.cursorGoto(e) case event.ScrollUp: w.scrollUp(e.Count) case event.ScrollDown: w.scrollDown(e.Count) case event.ScrollTop: w.scrollTop(e.Count) case event.ScrollTopHead: w.scrollTopHead(e.Count) case event.ScrollMiddle: w.scrollMiddle(e.Count) case event.ScrollMiddleHead: w.scrollMiddleHead(e.Count) case event.ScrollBottom: w.scrollBottom(e.Count) case event.ScrollBottomHead: w.scrollBottomHead(e.Count) case event.PageUp: w.pageUp() case event.PageDown: w.pageDown() case event.PageUpHalf: w.pageUpHalf() case event.PageDownHalf: w.pageDownHalf() case event.PageTop: w.pageTop() case event.PageEnd: w.pageEnd() case event.WindowTop: w.windowTop(e.Count) case event.WindowMiddle: w.windowMiddle() case event.WindowBottom: w.windowBottom(e.Count) case event.JumpTo: w.jumpTo() case event.JumpBack: w.jumpBack() case event.DeleteByte: newEvent = event.Event{Type: event.Copied, Buffer: w.deleteBytes(e.Count), Arg: "deleted"} case event.DeletePrevByte: newEvent = event.Event{Type: event.Copied, Buffer: w.deletePrevBytes(e.Count), Arg: "deleted"} case event.Increment: w.increment(e.Count) case event.Decrement: w.decrement(e.Count) case event.ShiftLeft: w.shiftLeft(e.Count) case event.ShiftRight: w.shiftRight(e.Count) case event.ShowBinary: if str := w.showBinary(); str != "" { newEvent = event.Event{Type: event.Info, Error: errors.New(str)} } case event.ShowDecimal: if str := w.showDecimal(); str != "" { newEvent = event.Event{Type: event.Info, Error: errors.New(str)} } case event.StartInsert: w.startInsert() case event.StartInsertHead: w.startInsertHead() case event.StartAppend: w.startAppend() case event.StartAppendEnd: w.startAppendEnd() case event.StartReplaceByte: w.startReplaceByte() case event.StartReplace: w.startReplace() case event.ExitInsert: w.exitInsert() case event.Rune: if w.insertRune(e.Mode, e.Rune) { newEvent = event.Event{Type: event.ExitInsert} } case event.Backspace: w.backspace(e.Mode) case event.Delete: w.deleteByte() case event.StartVisual: w.startVisual() case event.SwitchVisualEnd: w.switchVisualEnd() case event.ExitVisual: w.exitVisual() case event.SwitchFocus: w.focusText = !w.focusText if w.pending { w.pending = false w.pendingByte = '\x00' } case event.Undo: if e.Mode != mode.Normal { panic("event.Undo should be emitted under normal mode") } w.undo(e.Count) case event.Redo: if e.Mode != mode.Normal { panic("event.Undo should be emitted under normal mode") } w.redo(e.Count) case event.Copy: newEvent = event.Event{Type: event.Copied, Buffer: w.copy(), Arg: "yanked"} case event.Cut: newEvent = event.Event{Type: event.Copied, Buffer: w.cut(), Arg: "deleted"} case event.Paste, event.PastePrev: newEvent = event.Event{Type: event.Pasted, Count: w.paste(e)} case event.ExecuteSearch: w.search(e.Arg, e.Rune == '/') case event.NextSearch: 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 } changed := changedTick != w.changedTick if e.Type != event.Undo && e.Type != event.Redo { if (e.Mode == mode.Normal || e.Mode == mode.Visual) && changed || e.Type == event.ExitInsert && w.prevChanged { w.history.Push(w.buffer, w.offset, w.cursor, w.changedTick) } else if e.Mode != mode.Normal && e.Mode != mode.Visual && w.prevChanged && !changed && event.CursorUp <= e.Type && e.Type <= event.JumpBack { w.history.Push(w.buffer, offset, cursor, w.changedTick) } } w.prevChanged = changed w.mu.Unlock() if newEvent.Type == event.Nop { w.redrawCh <- struct{}{} } else { w.eventCh <- newEvent } } func (w *window) readByte(offset int64) (byte, error) { n, err := w.buffer.ReadAt(w.buf1[:], offset) if err != nil && err != io.EOF { return 0, err } if n == 0 { return 0, io.EOF } return w.buf1[0], nil } func (w *window) readBytes(offset int64, l int) (int, []byte, error) { var reused bool if l <= cap(w.buf) { w.buf, reused = w.buf[:l], true } else { w.buf = make([]byte, l) } n, err := w.buffer.ReadAt(w.buf, offset) if err != nil && err != io.EOF { return 0, w.buf, err } if reused { for i := n; i < len(w.buf); i++ { w.buf[i] = 0 } } return n, w.buf, nil } func (w *window) writeTo(r *event.Range, dst io.Writer) (int64, error) { w.mu.Lock() defer w.mu.Unlock() var from, to int64 if r == nil { from, to = 0, w.length-1 } else { var err error if from, err = w.positionToOffset(r.From); err != nil { return 0, err } if to, err = w.positionToOffset(r.To); err != nil { return 0, err } if from > to { from, to = to, from } } return io.Copy(dst, io.NewSectionReader(w.buffer, from, to-from+1)) } func (w *window) positionToOffset(pos event.Position) (int64, error) { var offset int64 switch pos := pos.(type) { case event.Absolute: offset = pos.Offset case event.Relative: offset = w.cursor + pos.Offset case event.End: offset = max(w.length, 1) - 1 + pos.Offset case event.VisualStart: if w.visualStart < 0 { return 0, errors.New("no visual selection found") } // TODO: save visualStart after exiting visual mode offset = w.visualStart + pos.Offset case event.VisualEnd: if w.visualStart < 0 { return 0, errors.New("no visual selection found") } offset = w.cursor + pos.Offset default: return 0, errors.New("invalid range") } return max(min(offset, max(w.length, 1)-1), 0), nil } func (w *window) state(width, height int) (*state.WindowState, error) { w.mu.Lock() defer w.mu.Unlock() w.setSize(width, height) n, bytes, err := w.readBytes(w.offset, int(w.height*w.width)) if err != nil { return nil, err } return &state.WindowState{ Name: w.name, Modified: w.changedTick != w.savedChangedTick, Width: int(w.width), Offset: w.offset, Cursor: w.cursor, Bytes: bytes, Size: n, Length: w.length, Pending: w.pending, PendingByte: w.pendingByte, VisualStart: w.visualStart, EditedIndices: w.buffer.EditedIndices(), FocusText: w.focusText, }, nil } func (w *window) updateTick() { w.maxChangedTick++ w.changedTick = w.maxChangedTick } func (w *window) insert(offset int64, c byte) { w.buffer.Insert(offset, c) w.updateTick() } func (w *window) replace(offset int64, c byte) { w.buffer.Replace(offset, c) w.updateTick() } func (w *window) undoReplace(offset int64) { w.buffer.UndoReplace(offset) w.updateTick() } func (w *window) replaceIn(start, end int64, c byte) { w.buffer.ReplaceIn(start, end, c) w.updateTick() } func (w *window) delete(offset int64) { w.buffer.Delete(offset) w.updateTick() } func (w *window) undo(count int64) { for range max(count, 1) { buffer, _, offset, cursor, tick := w.history.Undo() if buffer == nil { return } w.buffer, w.offset, w.cursor, w.changedTick = buffer, offset, cursor, tick w.length, _ = w.buffer.Len() } } func (w *window) redo(count int64) { for range max(count, 1) { buffer, offset, cursor, tick := w.history.Redo() if buffer == nil { return } w.buffer, w.offset, w.cursor, w.changedTick = buffer, offset, cursor, tick w.length, _ = w.buffer.Len() } } func (w *window) cursorUp(count int64) { w.cursor -= min(max(count, 1), w.cursor/w.width) * w.width if w.append && w.extending && w.cursor < w.length-1 { w.append = false w.extending = false if w.length > 0 { w.length-- } } } func (w *window) cursorDown(count int64) { w.cursor += min( min( max(count, 1), (max(w.length, 1)-1)/w.width-w.cursor/w.width, )*w.width, max(w.length, 1)-1-w.cursor) } func (w *window) cursorLeft(count int64) { w.cursor -= min(max(count, 1), w.cursor%w.width) if w.append && w.extending && w.cursor < w.length-1 { w.append = false w.extending = false if w.length > 0 { w.length-- } } } func (w *window) cursorRight(m mode.Mode, count int64) { if m != mode.Insert { w.cursor += min( min(max(count, 1), w.width-1-w.cursor%w.width), max(w.length, 1)-1-w.cursor, ) } else if !w.extending { w.cursor += min( min(max(count, 1), w.width-1-w.cursor%w.width), w.length-w.cursor, ) if w.cursor == w.length { w.append = true w.extending = true w.length++ } } } func (w *window) cursorPrev(count int64) { w.cursor -= min(max(count, 1), w.cursor) if w.append && w.extending && w.cursor != w.length { w.append = false w.extending = false if w.length > 0 { w.length-- } } } func (w *window) cursorNext(m mode.Mode, count int64) { if m != mode.Insert { w.cursor += min(max(count, 1), max(w.length, 1)-1-w.cursor) } else if !w.extending { w.cursor += min(max(count, 1), w.length-w.cursor) if w.cursor == w.length { w.append = true w.extending = true w.length++ } } } func (w *window) cursorHead(_ int64) { w.cursor -= w.cursor % w.width } func (w *window) cursorEnd(count int64) { w.cursor = min( (w.cursor/w.width+max(count, 1))*w.width-1, max(w.length, 1)-1, ) } func (w *window) cursorGoto(e event.Event) { if e.Range != nil { if e.Range.To != nil { w.cursorGotoPos(e.Range.To, e.CmdName) } else if e.Range.From != nil { w.cursorGotoPos(e.Range.From, e.CmdName) } } } func (w *window) cursorGotoPos(pos event.Position, cmdName string) { switch cmdName { case "go[to]": switch p := pos.(type) { case event.Absolute: pos = event.Absolute{Offset: p.Offset * w.width} case event.Relative: pos = event.Relative{Offset: p.Offset * w.width} case event.End: pos = event.End{Offset: p.Offset * w.width} case event.VisualStart: pos = event.VisualStart{Offset: p.Offset * w.width} case event.VisualEnd: pos = event.VisualEnd{Offset: p.Offset * w.width} } case "%": switch p := pos.(type) { case event.Absolute: pos = event.Absolute{Offset: p.Offset * w.length / 100} case event.Relative: pos = event.Relative{Offset: p.Offset * w.length / 100} case event.End: pos = event.End{Offset: p.Offset * w.length / 100} case event.VisualStart: pos = event.VisualStart{Offset: p.Offset * w.length / 100} case event.VisualEnd: pos = event.VisualEnd{Offset: p.Offset * w.length / 100} } } if offset, err := w.positionToOffset(pos); err == nil { w.cursor = offset if w.cursor < w.offset { w.offset = (max(w.cursor/w.width, w.height/2) - w.height/2) * w.width } else if w.cursor >= w.offset+w.height*w.width { h := (max(w.length, 1)+w.width-1)/w.width - w.height w.offset = min((w.cursor-w.height*w.width+w.width)/w.width+w.height/2, h) * w.width } } } func (w *window) scrollUp(count int64) { w.offset -= min(max(count, 1), w.offset/w.width) * w.width if w.cursor >= w.offset+w.height*w.width { w.cursor -= ((w.cursor-w.offset-w.height*w.width)/w.width + 1) * w.width } } func (w *window) scrollDown(count int64) { h := max((max(w.length, 1)+w.width-1)/w.width-w.height, 0) w.offset += min(max(count, 1), h-w.offset/w.width) * w.width if w.cursor < w.offset { w.cursor += min( (w.offset-w.cursor+w.width-1)/w.width*w.width, max(w.length, 1)-1-w.cursor, ) } } func (w *window) scrollTop(count int64) { if count > 0 { w.cursor = min( min( count*w.width+w.cursor%w.width, (max(w.length, 1)-1)/w.width*w.width+w.cursor%w.width, ), max(w.length, 1)-1, ) } w.offset = w.cursor / w.width * w.width } func (w *window) scrollTopHead(count int64) { w.cursorHead(0) w.scrollTop(count) } func (w *window) scrollMiddle(count int64) { if count > 0 { w.cursor = min( min( count*w.width+w.cursor%w.width, (max(w.length, 1)-1)/w.width*w.width+w.cursor%w.width, ), max(w.length, 1)-1, ) } w.offset = max(w.cursor/w.width-w.height/2, 0) * w.width } func (w *window) scrollMiddleHead(count int64) { w.cursorHead(0) w.scrollMiddle(count) } func (w *window) scrollBottom(count int64) { if count > 0 { w.cursor = min( min( count*w.width+w.cursor%w.width, (max(w.length, 1)-1)/w.width*w.width+w.cursor%w.width, ), max(w.length, 1)-1, ) } w.offset = max(w.cursor/w.width-w.height, 0) * w.width } func (w *window) scrollBottomHead(count int64) { w.cursorHead(0) w.scrollBottom(count) } func (w *window) pageUp() { w.offset = max(w.offset-(w.height-2)*w.width, 0) if w.offset == 0 { w.cursor = 0 } else if w.cursor >= w.offset+w.height*w.width { w.cursor = w.offset + (w.height-1)*w.width } } func (w *window) pageDown() { offset := max(((w.length+w.width-1)/w.width-w.height)*w.width, 0) w.offset = min(w.offset+(w.height-2)*w.width, offset) if w.cursor < w.offset { w.cursor = w.offset } else if w.offset == offset { w.cursor = ((max(w.length, 1)+w.width-1)/w.width - 1) * w.width } } func (w *window) pageUpHalf() { w.offset = max(w.offset-max(w.height/2, 1)*w.width, 0) if w.offset == 0 { w.cursor = 0 } else if w.cursor >= w.offset+w.height*w.width { w.cursor = w.offset + (w.height-1)*w.width } } func (w *window) pageDownHalf() { offset := max(((w.length+w.width-1)/w.width-w.height)*w.width, 0) w.offset = min(w.offset+max(w.height/2, 1)*w.width, offset) if w.cursor < w.offset { w.cursor = w.offset } else if w.offset == offset { w.cursor = ((max(w.length, 1)+w.width-1)/w.width - 1) * w.width } } func (w *window) pageTop() { w.offset = 0 w.cursor = 0 } func (w *window) pageEnd() { w.offset = max(((w.length+w.width-1)/w.width-w.height)*w.width, 0) w.cursor = ((max(w.length, 1)+w.width-1)/w.width - 1) * w.width } func (w *window) windowTop(count int64) { w.cursor = (w.offset/w.width + min( min(max(count, 1)-1, (w.length-w.offset)/w.width), max(w.height, 1)-1, )) * w.width } func (w *window) windowMiddle() { h := min((w.length-w.offset)/w.width, max(w.height, 1)-1) w.cursor = (w.offset/w.width + h/2) * w.width } func (w *window) windowBottom(count int64) { h := min((w.length-w.offset)/w.width, max(w.height, 1)-1) w.cursor = (w.offset/w.width + h - min(h, max(count, 1)-1)) * w.width } func (w *window) jumpTo() { i := min(w.cursor, 16) _, bytes, err := w.readBytes(w.cursor-i, 32) if err != nil { return } for ; i >= 0; i-- { if !unicode.IsDigit(rune(bytes[i])) { bytes = bytes[i+1:] break } } for i := 0; i < len(bytes); i++ { if !unicode.IsDigit(rune(bytes[i])) { bytes = bytes[:i] break } } offset, _ := strconv.ParseInt(string(bytes), 10, 64) if offset <= 0 || w.length <= offset { return } w.stack = append(w.stack, position{cursor: w.cursor, offset: w.offset}) w.cursor = offset w.offset = max(offset-offset%w.width-max(w.height/3, 0)*w.width, 0) } func (w *window) jumpBack() { if len(w.stack) == 0 { return } if pos := w.stack[len(w.stack)-1]; pos.cursor < w.length { w.cursor, w.offset = pos.cursor, pos.offset } w.stack = w.stack[:len(w.stack)-1] } func (w *window) deleteBytes(count int64) *buffer.Buffer { if w.length == 0 { return nil } count = min(max(count, 1), w.length-w.cursor) b := w.buffer.Copy(w.cursor, w.cursor+count) w.buffer.Cut(w.cursor, w.cursor+count) w.length, _ = w.buffer.Len() w.cursor = min(w.cursor, max(w.length, 1)-1) w.updateTick() return b } func (w *window) deletePrevBytes(count int64) *buffer.Buffer { if w.cursor == 0 { return nil } count = min(max(count, 1), w.cursor) b := w.buffer.Copy(w.cursor-count, w.cursor) w.buffer.Cut(w.cursor-count, w.cursor) w.length, _ = w.buffer.Len() w.cursor -= count w.updateTick() return b } func (w *window) increment(count int64) { b, err := w.readByte(w.cursor) if err != nil && err != io.EOF { return } w.replace(w.cursor, b+byte(max(count, 1))) if w.length == 0 { w.length++ } } func (w *window) decrement(count int64) { b, err := w.readByte(w.cursor) if err != nil && err != io.EOF { return } w.replace(w.cursor, b-byte(max(count, 1))) if w.length == 0 { w.length++ } } func (w *window) shiftLeft(count int64) { b, err := w.readByte(w.cursor) if err != nil && err != io.EOF { return } w.replace(w.cursor, b<>byte(max(count, 1))) if w.length == 0 { w.length++ } } func (w *window) showBinary() string { b, err := w.readByte(w.cursor) if err != nil { return "" } return fmt.Sprintf("%08b", b) } func (w *window) showDecimal() string { b, err := w.readByte(w.cursor) if err != nil { return "" } return strconv.FormatInt(int64(b), 10) } func (w *window) startInsert() { w.append = false w.extending = false w.pending = false if w.cursor == w.length { w.append = true w.extending = true w.length++ } } func (w *window) startInsertHead() { w.cursorHead(0) w.append = false w.extending = false w.pending = false if w.cursor == w.length { w.append = true w.extending = true w.length++ } } func (w *window) startAppend() { w.append = true w.extending = false w.pending = false if w.length > 0 { w.cursor++ } if w.cursor == w.length { w.extending = true w.length++ } } func (w *window) startAppendEnd() { w.cursorEnd(0) w.startAppend() } func (w *window) startReplaceByte() { w.replaceByte = true w.append = false w.extending = false w.pending = false } func (w *window) startReplace() { w.replaceByte = false w.append = true w.extending = false w.pending = false } func (w *window) exitInsert() { w.pending = false if w.append { if w.extending && w.length > 0 { w.length-- } if w.cursor > 0 { w.cursor-- } w.replaceByte = false w.append = false w.extending = false w.pending = false } w.buffer.Flush() } func (w *window) insertRune(m mode.Mode, ch rune) (exitInsert bool) { if m == mode.Insert || m == mode.Replace { if w.focusText { var buf [4]byte n := utf8.EncodeRune(buf[:], ch) for i := range n { exitInsert = exitInsert || w.insertByte(m, byte(buf[i]>>4)) exitInsert = exitInsert || w.insertByte(m, byte(buf[i]&0x0f)) } } else if '0' <= ch && ch <= '9' { exitInsert = w.insertByte(m, byte(ch-'0')) } else if 'a' <= ch && ch <= 'f' { exitInsert = w.insertByte(m, byte(ch-'a'+0x0a)) } } return } func (w *window) insertByte(m mode.Mode, b byte) bool { if w.pending { switch m { case mode.Insert: w.insert(w.cursor, w.pendingByte|b) w.cursor++ w.length++ case mode.Replace: if w.visualStart >= 0 && w.replaceByte { start, end := w.visualStart, w.cursor if start > end { start, end = end, start } w.replaceIn(start, end+1, w.pendingByte|b) w.visualStart = -1 return true } w.replace(w.cursor, w.pendingByte|b) if w.length == 0 { w.length++ } if w.replaceByte { w.exitInsert() return true } w.cursor++ if w.cursor == w.length { w.append = true w.extending = true w.length++ } } w.pending = false w.pendingByte = '\x00' } else { w.pending = true w.pendingByte = b << 4 } return false } func (w *window) backspace(m mode.Mode) { if w.pending { w.pending = false w.pendingByte = '\x00' } else if m == mode.Replace { if w.cursor > 0 { w.cursor-- w.undoReplace(w.cursor) } } else if w.cursor > 0 { w.delete(w.cursor - 1) w.cursor-- w.length-- } } func (w *window) deleteByte() { if w.length == 0 { return } w.delete(w.cursor) w.length-- if w.cursor == w.length && w.cursor > 0 { w.cursor-- } } func (w *window) startVisual() { w.visualStart = w.cursor } func (w *window) switchVisualEnd() { if w.visualStart < 0 { panic("window#switchVisualEnd should be called in visual mode") } w.cursor, w.visualStart = w.visualStart, w.cursor } func (w *window) exitVisual() { w.visualStart = -1 } func (w *window) copy() *buffer.Buffer { if w.visualStart < 0 { panic("window#copy should be called in visual mode") } start, end := w.visualStart, w.cursor if start > end { start, end = end, start } if end == w.length { return nil } w.visualStart = -1 w.cursor = start return w.buffer.Copy(start, end+1) } func (w *window) cut() *buffer.Buffer { if w.visualStart < 0 { panic("window#cut should be called in visual mode") } start, end := w.visualStart, w.cursor if start > end { start, end = end, start } if end == w.length { return nil } w.visualStart = -1 b := w.buffer.Copy(start, end+1) w.buffer.Cut(start, end+1) w.length, _ = w.buffer.Len() w.cursor = min(start, max(w.length, 1)-1) w.updateTick() return b } func (w *window) paste(e event.Event) int64 { count := max(e.Count, 1) pos := w.cursor if e.Type != event.PastePrev { pos = min(w.cursor+1, w.length) } for range count { w.buffer.Paste(pos, e.Buffer) } l, _ := e.Buffer.Len() w.length, _ = w.buffer.Len() w.cursor = min(max(pos+l*count-1, 0), max(w.length, 1)-1) w.updateTick() return l * count } 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() { switch x := (<-ch).(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) abortSearch() { if err := w.searcher.Abort(); err != nil { w.eventCh <- event.Event{Type: event.Info, Error: err} } } func (w *window) setPathName(path, name string) { w.path, w.name = path, name } func (w *window) getName() string { return cmp.Or(w.name, "[No Name]") }