star/bed/cmdline/cmdline.go

245 lines
5.3 KiB
Go
Raw Permalink Normal View History

2025-04-26 19:33:14 +08:00
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
}