245 lines
5.3 KiB
Go
245 lines
5.3 KiB
Go
|
package cmdline
|
||
|
|
||
|
import (
|
||
|
"slices"
|
||
|
"sync"
|
||
|
"unicode"
|
||
|
|
||
|
"b612.me/apps/b612/bed/event"
|
||
|
)
|
||
|
|
||
|
// Cmdline implements editor.Cmdline
|
||
|
type Cmdline struct {
|
||
|
cmdline []rune
|
||
|
cursor int
|
||
|
completor *completor
|
||
|
typ rune
|
||
|
historyIndex int
|
||
|
history []string
|
||
|
histories map[bool][]string
|
||
|
eventCh chan<- event.Event
|
||
|
cmdlineCh <-chan event.Event
|
||
|
redrawCh chan<- struct{}
|
||
|
mu *sync.Mutex
|
||
|
}
|
||
|
|
||
|
// NewCmdline creates a new Cmdline.
|
||
|
func NewCmdline() *Cmdline {
|
||
|
return &Cmdline{
|
||
|
completor: newCompletor(&filesystem{}, &environment{}),
|
||
|
histories: map[bool][]string{false: {}, true: {}},
|
||
|
mu: new(sync.Mutex),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Init initializes the Cmdline.
|
||
|
func (c *Cmdline) Init(eventCh chan<- event.Event, cmdlineCh <-chan event.Event, redrawCh chan<- struct{}) {
|
||
|
c.eventCh, c.cmdlineCh, c.redrawCh = eventCh, cmdlineCh, redrawCh
|
||
|
}
|
||
|
|
||
|
// Run the cmdline.
|
||
|
func (c *Cmdline) Run() {
|
||
|
for e := range c.cmdlineCh {
|
||
|
c.mu.Lock()
|
||
|
switch e.Type {
|
||
|
case event.StartCmdlineCommand:
|
||
|
c.start(':', e.Arg)
|
||
|
case event.StartCmdlineSearchForward:
|
||
|
c.start('/', "")
|
||
|
case event.StartCmdlineSearchBackward:
|
||
|
c.start('?', "")
|
||
|
case event.ExitCmdline:
|
||
|
c.clear()
|
||
|
case event.CursorUp:
|
||
|
c.cursorUp()
|
||
|
case event.CursorDown:
|
||
|
c.cursorDown()
|
||
|
case event.CursorLeft:
|
||
|
c.cursorLeft()
|
||
|
case event.CursorRight:
|
||
|
c.cursorRight()
|
||
|
case event.CursorHead:
|
||
|
c.cursorHead()
|
||
|
case event.CursorEnd:
|
||
|
c.cursorEnd()
|
||
|
case event.BackspaceCmdline:
|
||
|
c.backspace()
|
||
|
case event.DeleteCmdline:
|
||
|
c.deleteRune()
|
||
|
case event.DeleteWordCmdline:
|
||
|
c.deleteWord()
|
||
|
case event.ClearToHeadCmdline:
|
||
|
c.clearToHead()
|
||
|
case event.ClearCmdline:
|
||
|
c.clear()
|
||
|
case event.Rune:
|
||
|
c.insert(e.Rune)
|
||
|
case event.CompleteForwardCmdline:
|
||
|
c.complete(true)
|
||
|
c.redrawCh <- struct{}{}
|
||
|
c.mu.Unlock()
|
||
|
continue
|
||
|
case event.CompleteBackCmdline:
|
||
|
c.complete(false)
|
||
|
c.redrawCh <- struct{}{}
|
||
|
c.mu.Unlock()
|
||
|
continue
|
||
|
case event.ExecuteCmdline:
|
||
|
if c.execute() {
|
||
|
c.mu.Unlock()
|
||
|
continue
|
||
|
}
|
||
|
default:
|
||
|
c.mu.Unlock()
|
||
|
continue
|
||
|
}
|
||
|
c.completor.clear()
|
||
|
c.mu.Unlock()
|
||
|
c.redrawCh <- struct{}{}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorUp() {
|
||
|
if c.historyIndex--; c.historyIndex >= 0 {
|
||
|
c.cmdline = []rune(c.history[c.historyIndex])
|
||
|
c.cursor = len(c.cmdline)
|
||
|
} else {
|
||
|
c.clear()
|
||
|
c.historyIndex = -1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorDown() {
|
||
|
if c.historyIndex++; c.historyIndex < len(c.history) {
|
||
|
c.cmdline = []rune(c.history[c.historyIndex])
|
||
|
c.cursor = len(c.cmdline)
|
||
|
} else {
|
||
|
c.clear()
|
||
|
c.historyIndex = len(c.history)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorLeft() {
|
||
|
c.cursor = max(0, c.cursor-1)
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorRight() {
|
||
|
c.cursor = min(len(c.cmdline), c.cursor+1)
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorHead() {
|
||
|
c.cursor = 0
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) cursorEnd() {
|
||
|
c.cursor = len(c.cmdline)
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) backspace() {
|
||
|
if c.cursor > 0 {
|
||
|
c.cmdline = slices.Delete(c.cmdline, c.cursor-1, c.cursor)
|
||
|
c.cursor--
|
||
|
return
|
||
|
}
|
||
|
if len(c.cmdline) == 0 {
|
||
|
c.eventCh <- event.Event{Type: event.ExitCmdline}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) deleteRune() {
|
||
|
if c.cursor < len(c.cmdline) {
|
||
|
c.cmdline = slices.Delete(c.cmdline, c.cursor, c.cursor+1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) deleteWord() {
|
||
|
i := c.cursor
|
||
|
for i > 0 && unicode.IsSpace(c.cmdline[i-1]) {
|
||
|
i--
|
||
|
}
|
||
|
if i > 0 {
|
||
|
isk := isKeyword(c.cmdline[i-1])
|
||
|
for i > 0 && isKeyword(c.cmdline[i-1]) == isk && !unicode.IsSpace(c.cmdline[i-1]) {
|
||
|
i--
|
||
|
}
|
||
|
}
|
||
|
c.cmdline = slices.Delete(c.cmdline, i, c.cursor)
|
||
|
c.cursor = i
|
||
|
}
|
||
|
|
||
|
func isKeyword(c rune) bool {
|
||
|
return unicode.IsDigit(c) || unicode.IsLetter(c) || c == '_'
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) start(typ rune, arg string) {
|
||
|
c.typ = typ
|
||
|
c.cmdline = []rune(arg)
|
||
|
c.cursor = len(c.cmdline)
|
||
|
c.history = c.histories[typ == ':']
|
||
|
c.historyIndex = len(c.history)
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) clear() {
|
||
|
c.cmdline = []rune{}
|
||
|
c.cursor = 0
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) clearToHead() {
|
||
|
c.cmdline = slices.Delete(c.cmdline, 0, c.cursor)
|
||
|
c.cursor = 0
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) insert(ch rune) {
|
||
|
if unicode.IsPrint(ch) {
|
||
|
c.cmdline = slices.Insert(c.cmdline, c.cursor, ch)
|
||
|
c.cursor++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) complete(forward bool) {
|
||
|
c.cmdline = []rune(c.completor.complete(string(c.cmdline), forward))
|
||
|
c.cursor = len(c.cmdline)
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) execute() (finish bool) {
|
||
|
defer c.saveHistory()
|
||
|
switch c.typ {
|
||
|
case ':':
|
||
|
cmd, r, bang, _, _, arg, err := parse(string(c.cmdline))
|
||
|
if err != nil {
|
||
|
c.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else if cmd.name != "" {
|
||
|
c.eventCh <- event.Event{Type: cmd.eventType, Range: r, CmdName: cmd.name, Bang: bang, Arg: arg}
|
||
|
finish = cmd.eventType == event.QuitAll || cmd.eventType == event.QuitErr
|
||
|
}
|
||
|
case '/':
|
||
|
c.eventCh <- event.Event{Type: event.ExecuteSearch, Arg: string(c.cmdline), Rune: '/'}
|
||
|
case '?':
|
||
|
c.eventCh <- event.Event{Type: event.ExecuteSearch, Arg: string(c.cmdline), Rune: '?'}
|
||
|
default:
|
||
|
panic("cmdline.Cmdline.execute: unreachable")
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (c *Cmdline) saveHistory() {
|
||
|
cmdline := string(c.cmdline)
|
||
|
if cmdline == "" {
|
||
|
return
|
||
|
}
|
||
|
for i, h := range c.history {
|
||
|
if h == cmdline {
|
||
|
c.history = slices.Delete(c.history, i, i+1)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
c.histories[c.typ == ':'] = append(c.history, cmdline)
|
||
|
}
|
||
|
|
||
|
// Get returns the current state of cmdline.
|
||
|
func (c *Cmdline) Get() ([]rune, int, []string, int) {
|
||
|
c.mu.Lock()
|
||
|
defer c.mu.Unlock()
|
||
|
return c.cmdline, c.cursor, c.completor.results, c.completor.index
|
||
|
}
|