346 lines
8.0 KiB
Go
346 lines
8.0 KiB
Go
|
package editor
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"b612.me/apps/b612/bed/buffer"
|
||
|
"b612.me/apps/b612/bed/event"
|
||
|
"b612.me/apps/b612/bed/mode"
|
||
|
"b612.me/apps/b612/bed/state"
|
||
|
)
|
||
|
|
||
|
// Editor is the main struct for this command.
|
||
|
type Editor struct {
|
||
|
ui UI
|
||
|
wm Manager
|
||
|
cmdline Cmdline
|
||
|
mode mode.Mode
|
||
|
prevMode mode.Mode
|
||
|
searchTarget string
|
||
|
searchMode rune
|
||
|
prevEventType event.Type
|
||
|
buffer *buffer.Buffer
|
||
|
err error
|
||
|
errtyp int
|
||
|
cmdEventCh chan event.Event
|
||
|
wmEventCh chan event.Event
|
||
|
uiEventCh chan event.Event
|
||
|
redrawCh chan struct{}
|
||
|
cmdlineCh chan event.Event
|
||
|
quitCh chan struct{}
|
||
|
mu *sync.Mutex
|
||
|
}
|
||
|
|
||
|
// NewEditor creates a new editor.
|
||
|
func NewEditor(ui UI, wm Manager, cmdline Cmdline) *Editor {
|
||
|
return &Editor{
|
||
|
ui: ui,
|
||
|
wm: wm,
|
||
|
cmdline: cmdline,
|
||
|
mode: mode.Normal,
|
||
|
prevMode: mode.Normal,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Init initializes the editor.
|
||
|
func (e *Editor) Init() error {
|
||
|
e.cmdEventCh = make(chan event.Event)
|
||
|
e.wmEventCh = make(chan event.Event)
|
||
|
e.uiEventCh = make(chan event.Event)
|
||
|
e.redrawCh = make(chan struct{})
|
||
|
e.cmdlineCh = make(chan event.Event)
|
||
|
e.cmdline.Init(e.cmdEventCh, e.cmdlineCh, e.redrawCh)
|
||
|
e.quitCh = make(chan struct{})
|
||
|
e.wm.Init(e.wmEventCh, e.redrawCh)
|
||
|
e.mu = new(sync.Mutex)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *Editor) listen() error {
|
||
|
var wg sync.WaitGroup
|
||
|
errCh := make(chan error, 1)
|
||
|
wg.Add(1)
|
||
|
go func() {
|
||
|
defer wg.Done()
|
||
|
for {
|
||
|
select {
|
||
|
case <-e.redrawCh:
|
||
|
_ = e.redraw()
|
||
|
case <-e.quitCh:
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
wg.Add(1)
|
||
|
go func() {
|
||
|
defer wg.Done()
|
||
|
for {
|
||
|
select {
|
||
|
case ev := <-e.wmEventCh:
|
||
|
if redraw, finish, err := e.emit(ev); redraw {
|
||
|
e.redrawCh <- struct{}{}
|
||
|
} else if finish {
|
||
|
close(e.quitCh)
|
||
|
errCh <- err
|
||
|
}
|
||
|
case <-e.quitCh:
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
wg.Add(1)
|
||
|
go func() {
|
||
|
defer wg.Done()
|
||
|
for {
|
||
|
select {
|
||
|
case ev := <-e.cmdEventCh:
|
||
|
if redraw, finish, err := e.emit(ev); redraw {
|
||
|
e.redrawCh <- struct{}{}
|
||
|
} else if finish {
|
||
|
close(e.quitCh)
|
||
|
errCh <- err
|
||
|
}
|
||
|
case ev := <-e.uiEventCh:
|
||
|
if redraw, finish, err := e.emit(ev); redraw {
|
||
|
e.redrawCh <- struct{}{}
|
||
|
} else if finish {
|
||
|
close(e.quitCh)
|
||
|
errCh <- err
|
||
|
}
|
||
|
case <-e.quitCh:
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
wg.Wait()
|
||
|
select {
|
||
|
case err := <-errCh:
|
||
|
return err
|
||
|
default:
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type quitErr struct {
|
||
|
code int
|
||
|
}
|
||
|
|
||
|
func (err *quitErr) Error() string {
|
||
|
return "exit with " + strconv.Itoa(err.code)
|
||
|
}
|
||
|
|
||
|
func (err *quitErr) ExitCode() int {
|
||
|
return err.code
|
||
|
}
|
||
|
|
||
|
func (e *Editor) emit(ev event.Event) (redraw, finish bool, err error) {
|
||
|
e.mu.Lock()
|
||
|
if ev.Type != event.Redraw {
|
||
|
e.prevEventType = ev.Type
|
||
|
}
|
||
|
switch ev.Type {
|
||
|
case event.QuitAll:
|
||
|
if ev.Arg != "" {
|
||
|
e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError
|
||
|
redraw = true
|
||
|
} else {
|
||
|
finish = true
|
||
|
}
|
||
|
case event.QuitErr:
|
||
|
args := strings.Fields(ev.Arg)
|
||
|
if len(args) > 1 {
|
||
|
e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError
|
||
|
redraw = true
|
||
|
} else if len(args) > 0 {
|
||
|
n, er := strconv.Atoi(args[0])
|
||
|
if er != nil {
|
||
|
e.err, e.errtyp = fmt.Errorf("invalid argument for %s: %w", ev.CmdName, er), state.MessageError
|
||
|
redraw = true
|
||
|
} else {
|
||
|
err = &quitErr{n}
|
||
|
finish = true
|
||
|
}
|
||
|
} else {
|
||
|
err = &quitErr{1}
|
||
|
finish = true
|
||
|
}
|
||
|
case event.Suspend:
|
||
|
e.mu.Unlock()
|
||
|
if err := suspend(e); err != nil {
|
||
|
e.mu.Lock()
|
||
|
e.err, e.errtyp = err, state.MessageError
|
||
|
e.mu.Unlock()
|
||
|
}
|
||
|
redraw = true
|
||
|
return
|
||
|
case event.Info:
|
||
|
e.err, e.errtyp = ev.Error, state.MessageInfo
|
||
|
redraw = true
|
||
|
case event.Error:
|
||
|
e.err, e.errtyp = ev.Error, state.MessageError
|
||
|
redraw = true
|
||
|
case event.Redraw:
|
||
|
width, height := e.ui.Size()
|
||
|
e.wm.Resize(width, height-1)
|
||
|
redraw = true
|
||
|
case event.Copied:
|
||
|
e.mode, e.prevMode = mode.Normal, e.mode
|
||
|
if ev.Buffer != nil {
|
||
|
e.buffer = ev.Buffer
|
||
|
if l, err := e.buffer.Len(); err != nil {
|
||
|
e.err, e.errtyp = err, state.MessageError
|
||
|
} else {
|
||
|
e.err, e.errtyp = fmt.Errorf("%[1]d (0x%[1]x) bytes %[2]s", l, ev.Arg), state.MessageInfo
|
||
|
}
|
||
|
}
|
||
|
redraw = true
|
||
|
case event.Pasted:
|
||
|
e.err, e.errtyp = fmt.Errorf("%[1]d (0x%[1]x) bytes pasted", ev.Count), state.MessageInfo
|
||
|
redraw = true
|
||
|
default:
|
||
|
switch ev.Type {
|
||
|
case event.StartInsert, event.StartInsertHead, event.StartAppend, event.StartAppendEnd:
|
||
|
e.mode, e.prevMode = mode.Insert, e.mode
|
||
|
case event.StartReplaceByte, event.StartReplace:
|
||
|
e.mode, e.prevMode = mode.Replace, e.mode
|
||
|
case event.ExitInsert:
|
||
|
e.mode, e.prevMode = mode.Normal, e.mode
|
||
|
case event.StartVisual:
|
||
|
e.mode, e.prevMode = mode.Visual, e.mode
|
||
|
case event.ExitVisual:
|
||
|
e.mode, e.prevMode = mode.Normal, e.mode
|
||
|
case event.StartCmdlineCommand:
|
||
|
if e.mode == mode.Visual {
|
||
|
ev.Arg = "'<,'>"
|
||
|
} else if ev.Count > 0 {
|
||
|
ev.Arg = ".,.+" + strconv.FormatInt(ev.Count-1, 10)
|
||
|
}
|
||
|
e.mode, e.prevMode = mode.Cmdline, e.mode
|
||
|
e.err = nil
|
||
|
case event.StartCmdlineSearchForward:
|
||
|
e.mode, e.prevMode = mode.Search, e.mode
|
||
|
e.err = nil
|
||
|
e.searchMode = '/'
|
||
|
case event.StartCmdlineSearchBackward:
|
||
|
e.mode, e.prevMode = mode.Search, e.mode
|
||
|
e.err = nil
|
||
|
e.searchMode = '?'
|
||
|
case event.ExitCmdline:
|
||
|
e.mode, e.prevMode = mode.Normal, e.mode
|
||
|
case event.ExecuteCmdline:
|
||
|
m := mode.Normal
|
||
|
if e.mode == mode.Search {
|
||
|
m = e.prevMode
|
||
|
}
|
||
|
e.mode, e.prevMode = m, e.mode
|
||
|
case event.ExecuteSearch:
|
||
|
e.searchTarget, e.searchMode = ev.Arg, ev.Rune
|
||
|
case event.NextSearch:
|
||
|
ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil
|
||
|
case event.PreviousSearch:
|
||
|
ev.Arg, ev.Rune, e.err = e.searchTarget, e.searchMode, nil
|
||
|
case event.Paste, event.PastePrev:
|
||
|
if e.buffer == nil {
|
||
|
e.mu.Unlock()
|
||
|
return
|
||
|
}
|
||
|
ev.Buffer = e.buffer
|
||
|
}
|
||
|
if e.mode == mode.Cmdline || e.mode == mode.Search ||
|
||
|
ev.Type == event.ExitCmdline || ev.Type == event.ExecuteCmdline {
|
||
|
e.mu.Unlock()
|
||
|
e.cmdlineCh <- ev
|
||
|
} else {
|
||
|
if event.ScrollUp <= ev.Type && ev.Type <= event.SwitchFocus {
|
||
|
e.prevMode, e.err = e.mode, nil
|
||
|
}
|
||
|
ev.Mode = e.mode
|
||
|
width, height := e.ui.Size()
|
||
|
e.wm.Resize(width, height-1)
|
||
|
e.mu.Unlock()
|
||
|
e.wm.Emit(ev)
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
e.mu.Unlock()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Open opens a new file.
|
||
|
func (e *Editor) Open(name string) error {
|
||
|
return e.wm.Open(name)
|
||
|
}
|
||
|
|
||
|
// OpenEmpty creates a new window.
|
||
|
func (e *Editor) OpenEmpty() error {
|
||
|
return e.wm.Open("")
|
||
|
}
|
||
|
|
||
|
// Read [io.Reader] and creates a new window.
|
||
|
func (e *Editor) Read(r io.Reader) error {
|
||
|
return e.wm.Read(r)
|
||
|
}
|
||
|
|
||
|
// Run the editor.
|
||
|
func (e *Editor) Run() error {
|
||
|
if err := e.ui.Init(e.uiEventCh); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if err := e.redraw(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
go e.ui.Run(defaultKeyManagers())
|
||
|
go e.cmdline.Run()
|
||
|
return e.listen()
|
||
|
}
|
||
|
|
||
|
func (e *Editor) redraw() (err error) {
|
||
|
e.mu.Lock()
|
||
|
defer e.mu.Unlock()
|
||
|
var s state.State
|
||
|
var windowIndex int
|
||
|
s.WindowStates, s.Layout, windowIndex, err = e.wm.State()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if s.WindowStates[windowIndex] == nil {
|
||
|
return errors.New("index out of windows")
|
||
|
}
|
||
|
s.WindowStates[windowIndex].Mode = e.mode
|
||
|
s.Mode, s.PrevMode, s.Error, s.ErrorType = e.mode, e.prevMode, e.err, e.errtyp
|
||
|
if s.Mode != mode.Visual && s.PrevMode != mode.Visual {
|
||
|
for _, ws := range s.WindowStates {
|
||
|
ws.VisualStart = -1
|
||
|
}
|
||
|
}
|
||
|
s.Cmdline, s.CmdlineCursor, s.CompletionResults, s.CompletionIndex = e.cmdline.Get()
|
||
|
if e.mode == mode.Search || e.prevEventType == event.ExecuteSearch {
|
||
|
s.SearchMode = e.searchMode
|
||
|
} else if e.prevEventType == event.NextSearch {
|
||
|
s.SearchMode, s.Cmdline = e.searchMode, []rune(e.searchTarget)
|
||
|
} else if e.prevEventType == event.PreviousSearch {
|
||
|
if e.searchMode == '/' {
|
||
|
s.SearchMode, s.Cmdline = '?', []rune(e.searchTarget)
|
||
|
} else {
|
||
|
s.SearchMode, s.Cmdline = '/', []rune(e.searchTarget)
|
||
|
}
|
||
|
}
|
||
|
return e.ui.Redraw(s)
|
||
|
}
|
||
|
|
||
|
// Close terminates the editor.
|
||
|
func (e *Editor) Close() error {
|
||
|
close(e.cmdEventCh)
|
||
|
close(e.wmEventCh)
|
||
|
close(e.uiEventCh)
|
||
|
close(e.redrawCh)
|
||
|
close(e.cmdlineCh)
|
||
|
e.wm.Close()
|
||
|
return e.ui.Close()
|
||
|
}
|