Add parser

This commit is contained in:
2026-03-27 23:50:04 +09:00
parent dbba404268
commit 52e974a721
12 changed files with 525 additions and 279 deletions

147
internal/editor/cmd.go Normal file
View File

@@ -0,0 +1,147 @@
package editor
type CmdKind int
const (
CmdMoveLeft CmdKind = iota
CmdMoveDown
CmdMoveUp
CmdMoveRight
CmdMoveToStart
CmdMoveToEnd
CmdMoveToNonBlank
CmdMoveToColumn
CmdMoveByWord
CmdMoveBackwardByWord
CmdMoveToEndOfWord
CmdMoveByLooseWord
CmdMoveBackwardByLooseWord
CmdMoveToEndOfLooseWord
CmdMoveToNonBlankOfNextLine
CmdMoveToNonBlankOfPrevLine
CmdMoveToLastLine
CmdMoveToLine
CmdMoveBySentence
CmdMoveBackwardBySentence
CmdMoveByParagraph
CmdMoveBackwardByParagraph
CmdMoveBySection
CmdMoveBackwardBySection
CmdMoveToTopOfView
CmdMoveToMiddleOfView
CmdMoveToBottomOfView
CmdMoveToBelowTopOfView
CmdMoveToAboveBottomOfView
CmdMarkSet
CmdMarkMoveTo
CmdMarkMoveToLine
CmdMarkBack
CmdMarkBackToLine
CmdViewDown
CmdViewUp
CmdViewDownHalf
CmdViewUpHalf
CmdViewDownLine
CmdViewUpLine
CmdViewToTop
CmdViewToMiddle
CmdViewToBottom
CmdViewRedraw
CmdSearchForward
CmdSearchBackward
CmdSearchNextMatch
CmdSearchPrevMatch
CmdSearchRepeatForward
CmdSearchRepeatBackward
CmdFindForward
CmdFindBackward
CmdFindBeforeForward
CmdFindBeforeBackward
CmdFindNextMatch
CmdFindPrevMatch
CmdInsertBefore
CmdInsertAfter
CmdInsertBeforeNonBlank
CmdInsertAfterEnd
CmdInsertOverwrite
CmdInsertOpenBelow
CmdInsertOpenAbove
CmdOpCopyLine
CmdOpCopyRegion
CmdOpCopyLineRegion
CmdOpCopyWord
CmdOpCopyToEnd
CmdOpCopyLineIntoReg
CmdOpPaste
CmdOpPasteBefore
CmdOpPasteFromReg
CmdOpDelete
CmdOpDeleteBefore
CmdOpDeleteLine
CmdOpDelteRegion
CmdOpDeleteLineRegion
CmdOpDeleteWord
CmdOpDelteToEnd
CmdOpChangeLine
CmdOpChangeRegion
CmdOpChangeLineRegion
CmdOpChangeWord
CmdOpChangeToEnd
CmdOpSubst
CmdOpSubstLine
CmdEditReplace
CmdEditJoin
CmdEditIndent
CmdEditOutdent
CmdEditIndentRegion
CmdEditOutdentRegion
CmdMiscShowInfo
CmdMiscRepeat
CmdMiscUndo
CmdMiscRestore
CmdMiscSaveAndQuit
CmdPromptMoveToLine
CmdPromptSaveAndQuit
CmdPromptSave
CmdPromptForceSave
CmdPromptQuit
CmdPromptForceQuit
CmdPromptOpen
CmdPromptForceOpen
CmdPromptRead
CmdPromptNext
CmdPromptPrev
CmdPromptShell
CmdPromptSaveAll
CmdPromptQuitAll
CmdPromptForceQuitAll
)
type Cmd struct {
Kind CmdKind
Num int
}

View File

@@ -1,41 +0,0 @@
package editor
type Combuf struct {
buf []rune
cache string
}
const maxCombufLen = 256
func NewCombuf() *Combuf {
return &Combuf{
buf: make([]rune, 0),
cache: "",
}
}
func (cb *Combuf) String() string {
return string(cb.buf)
}
func (cb *Combuf) InsertRune(r rune) {
cb.buf = append(cb.buf, r)
cb.cache = cb.String()
}
func (cb *Combuf) Clear() {
if len(cb.buf) > maxCombufLen {
cb.buf = make([]rune, 0)
} else {
cb.buf = cb.buf[:0]
}
}
func (cb *Combuf) Cache() string {
return cb.cache
}
func (cb *Combuf) ClearAll() {
cb.Clear()
cb.cache = ""
}

View File

@@ -1,49 +0,0 @@
package editor
// key: i
func (ed *Editor) InsertBefore() {
if ed.mode == ModeInsert {
panic("invalid state")
}
ed.ins.Init(ed.CurrentLine(), ed.col)
ed.mode = ModeInsert
}
// key: a
func (ed *Editor) InsertAfter() {
if ed.mode == ModeInsert {
panic("invalid state")
}
rc := ed.RuneCount()
if ed.col >= rc-1 {
ed.col = rc
} else {
ed.MoveRight(1)
}
ed.InsertBefore()
}
// key: x
func (ed *Editor) OpDelete(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
if len(ed.CurrentLine()) < 1 {
ed.Ring()
return
}
rs := []rune(ed.CurrentLine())
if ed.col < 1 {
ed.lines[ed.row] = string(rs[1:])
} else {
head := string(rs[:ed.col])
tail := string(rs[ed.col+1:])
ed.lines[ed.row] = head + tail
}
ed.Confine()
}
// key: ZZ
func (ed *Editor) MiscSaveAndQuit() {
ed.quit = true
}

View File

@@ -23,11 +23,11 @@ type Editor struct {
w, h int
x, y int
lines []string
ins *Insert
inp *Input
mode Mode
path string
bell bool
combuf *Combuf
parser *Parser
quit bool
}
@@ -63,11 +63,11 @@ func Init(args []string) *Editor {
x: 0,
y: 0,
lines: make([]string, 1),
ins: NewInsert(),
inp: NewInput(),
mode: ModeCommand,
path: path,
bell: false,
combuf: NewCombuf(),
parser: NewParser(),
quit: false,
}
@@ -104,7 +104,7 @@ func (ed *Editor) Finish() {
func (ed *Editor) Line(row int) string {
if ed.mode == ModeInsert && row == ed.row {
return ed.ins.Line()
return ed.inp.Line()
} else {
return ed.lines[row]
}
@@ -142,8 +142,8 @@ func (ed *Editor) InsertRune(r rune) {
if ed.mode != ModeInsert {
panic("invalid state")
}
ed.ins.WriteRune(r)
ed.col = ed.ins.Column()
ed.inp.WriteRune(r)
ed.col = ed.inp.Column()
}
func (ed *Editor) Ring() {

69
internal/editor/input.go Normal file
View File

@@ -0,0 +1,69 @@
package editor
import (
"unicode/utf8"
)
type Input struct {
head, tail string
body *RuneBuf
}
const maxBodyLen = 1024
func NewInput() *Input {
return &Input{
head: "",
tail: "",
body: new(RuneBuf),
}
}
func (inp *Input) Reset() {
inp.head = ""
inp.tail = ""
if inp.body.Len() > maxBodyLen {
inp.body = new(RuneBuf)
} else {
inp.body.Reset()
}
}
func (inp *Input) Init(line string, col int) {
inp.Reset()
rs := []rune(line)
inp.head = string(rs[:col])
if col < len(rs) {
inp.tail = string(rs[col:])
} else {
inp.tail = ""
}
}
func (inp *Input) WriteRune(r rune) {
inp.body.WriteRune(r)
}
func (inp *Input) Line() string {
return inp.head + inp.body.String() + inp.tail
}
func (inp *Input) Newline() []string {
lines := []string{
inp.head + inp.body.String(),
inp.tail,
}
inp.head = ""
inp.body.Reset()
// tail is intentionally preserved
return lines
}
func (inp *Input) Column() int {
s := inp.head + inp.body.String()
return utf8.RuneCountInString(s)
}
func (inp *Input) Backspace() bool {
return inp.body.Backspace()
}

View File

@@ -1,69 +1,24 @@
package editor
import (
"unicode/utf8"
)
type Insert struct {
head, tail string
body *RuneBuf
}
const maxBodyLen = 1024
func NewInsert() *Insert {
return &Insert{
head: "",
tail: "",
body: new(RuneBuf),
// i : Switch to insert mode before cursor.
func (ed *Editor) InsertBefore(n int) {
if ed.mode == ModeInsert {
panic("invalid state")
}
ed.inp.Init(ed.CurrentLine(), ed.col)
ed.mode = ModeInsert
}
func (ins *Insert) Reset() {
ins.head = ""
ins.tail = ""
if ins.body.Len() > maxBodyLen {
ins.body = new(RuneBuf)
// a : Switch to insert mode after cursor.
func (ed *Editor) InsertAfter(n int) {
if ed.mode == ModeInsert {
panic("invalid state")
}
rc := ed.RuneCount()
if ed.col >= rc-1 {
ed.col = rc
} else {
ins.body.Reset()
ed.MoveRight(1)
}
}
func (ins *Insert) Init(line string, col int) {
ins.Reset()
rs := []rune(line)
ins.head = string(rs[:col])
if col < len(rs) {
ins.tail = string(rs[col:])
} else {
ins.tail = ""
}
}
func (ins *Insert) WriteRune(r rune) {
ins.body.WriteRune(r)
}
func (ins *Insert) Line() string {
return ins.head + ins.body.String() + ins.tail
}
func (ins *Insert) Newline() []string {
lines := []string{
ins.head + ins.body.String(),
ins.tail,
}
ins.head = ""
ins.body.Reset()
// tail is intentionally preserved
return lines
}
func (ins *Insert) Column() int {
s := ins.head + ins.body.String()
return utf8.RuneCountInString(s)
}
func (ins *Insert) Backspace() bool {
return ins.body.Backspace()
ed.InsertBefore(n)
}

View File

@@ -1,9 +1,6 @@
package editor
import (
"regexp"
"strconv"
"tea.kareha.org/lab/termi"
)
@@ -11,8 +8,8 @@ func (ed *Editor) ExitInsert() {
if ed.mode != ModeInsert {
panic("invalid state")
}
ed.lines[ed.row] = ed.ins.Line()
ed.ins.Reset()
ed.lines[ed.row] = ed.inp.Line()
ed.inp.Reset()
ed.mode = ModeCommand
ed.MoveLeft(1)
}
@@ -29,7 +26,7 @@ func (ed *Editor) InsertNewline() {
} else {
after = []string{}
}
lines := ed.ins.Newline()
lines := ed.inp.Newline()
ed.lines = append(append(before, lines...), after...)
ed.row++
ed.col = 0
@@ -40,7 +37,7 @@ func (ed *Editor) Backspace() {
if ed.mode != ModeInsert {
panic("invalid state")
}
if !ed.ins.Backspace() {
if !ed.inp.Backspace() {
ed.Ring()
return
}
@@ -48,59 +45,6 @@ func (ed *Editor) Backspace() {
// col is already confined
}
var cmdRe = regexp.MustCompile("^(\\d*)([:mziaIARoOdyYxXDsScCpPrJ><\\.uUZ]*)(\\d*)([hjkl0\\$\\^|wbeWBE\\n\\+\\-G\\)\\(\\}\\{\\]\\[HML'`/?nNfFtT;,g]*)(.*?)$")
var letterRe = regexp.MustCompile("([m'`fFtT;,r])(.)$")
var letterSubRe = regexp.MustCompile("[fFtT;,]")
func (ed *Editor) Run(noNum bool, num int, op string, noSubnub bool, subnum int, mv string, letter string, replay bool) bool {
switch op {
case "x":
ed.OpDelete(num)
return true
}
switch op {
case "i":
ed.InsertBefore()
return true
case "a":
ed.InsertAfter()
return true
case "ZZ":
ed.MiscSaveAndQuit()
return true
}
switch mv {
case "h":
ed.MoveLeft(num)
return true
case "j":
ed.MoveDown(num)
return true
case "k":
ed.MoveUp(num)
return true
case "l":
ed.MoveRight(num)
return true
case "0":
ed.MoveToStart()
return true
case "$":
ed.MoveToEnd()
return true
case "^":
ed.MoveToNonBlank()
return true
case "|":
ed.MoveToColumn(num)
return true
}
return false
}
func (ed *Editor) Main() {
for !ed.quit {
ed.Draw()
@@ -111,71 +55,17 @@ func (ed *Editor) Main() {
switch key.Kind {
case termi.KeyRune:
if key.Rune == termi.RuneEscape {
ed.combuf.ClearAll()
ed.parser.ClearAll()
ed.Ring()
continue
}
ed.combuf.InsertRune(key.Rune)
ed.parser.InsertRune(key.Rune)
comb := ed.combuf.String()
m := cmdRe.FindStringSubmatch(comb)
/*
if len(m) < 1 {
// TODO error "not (yet) a vi command [" + comb + "]"
ed.combuf.Clear()
continue
c, ok := ed.parser.Parse()
if ok {
if ed.Run(c) {
ed.parser.Clear()
}
*/
var numStr, op, subnumStr, mv string
if len(m) > 0 {
numStr, op, subnumStr, mv = m[1], m[2], m[3], m[4]
}
m = letterRe.FindStringSubmatch(comb)
var letterCommand, letter string
if len(m) > 0 {
letterCommand, letter = m[1], m[2]
}
if letterCommand != "" {
if letterCommand == "m" || letterCommand == "r" {
op = letterCommand
mv = ""
} else if letterCommand == "'" || letterCommand == "`" {
mv = letterCommand
} else if letterSubRe.MatchString(letterCommand) {
mv = letterCommand
}
}
noNum := false
num := 1
if numStr == "" {
noNum = true
} else if numStr == "0" {
mv = "0"
} else {
n, err := strconv.Atoi(numStr)
if err != nil {
panic(err)
}
num = n
}
noSubnum := false
subnum := 1
if subnumStr == "" {
noSubnum = true
} else if subnumStr == "0" {
mv = "0"
} else {
n, err := strconv.Atoi(subnumStr)
if err != nil {
panic(err)
}
subnum = n
}
if ed.Run(noNum, num, op, noSubnum, subnum, mv, letter, false) {
ed.combuf.Clear()
}
case termi.KeyUp:
ed.MoveUp(1)

6
internal/editor/misc.go Normal file
View File

@@ -0,0 +1,6 @@
package editor
// ZZ : Save and quit.
func (ed *Editor) MiscSaveAndQuit() {
ed.quit = true
}

21
internal/editor/op.go Normal file
View File

@@ -0,0 +1,21 @@
package editor
// x : Delete character under cursor.
func (ed *Editor) OpDelete(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
if len(ed.CurrentLine()) < 1 {
ed.Ring()
return
}
rs := []rune(ed.CurrentLine())
if ed.col < 1 {
ed.lines[ed.row] = string(rs[1:])
} else {
head := string(rs[:ed.col])
tail := string(rs[ed.col+1:])
ed.lines[ed.row] = head + tail
}
ed.Confine()
}

192
internal/editor/parser.go Normal file
View File

@@ -0,0 +1,192 @@
package editor
import (
"strconv"
)
type Parser struct {
buf []rune
cache string
}
const maxParserLen = 256
func NewParser() *Parser {
return &Parser{
buf: make([]rune, 0),
cache: "",
}
}
func (p *Parser) String() string {
return string(p.buf)
}
func (p *Parser) InsertRune(r rune) {
p.buf = append(p.buf, r)
p.cache = p.String()
}
func (p *Parser) Clear() {
if len(p.buf) > maxParserLen {
p.buf = make([]rune, 0)
} else {
p.buf = p.buf[:0]
}
}
func (p *Parser) Cache() string {
return p.cache
}
func (p *Parser) ClearAll() {
p.Clear()
p.cache = ""
}
func (p *Parser) ParseMove(noNum bool, num int, op string) (Cmd, bool) {
switch op {
case "h":
return Cmd{
Kind: CmdMoveLeft,
Num: num,
}, true
case "j":
return Cmd{
Kind: CmdMoveDown,
Num: num,
}, true
case "k":
return Cmd{
Kind: CmdMoveUp,
Num: num,
}, true
case "l":
return Cmd{
Kind: CmdMoveRight,
Num: num,
}, true
case "0": // special
return Cmd{Kind: CmdMoveToStart}, true
case "$":
return Cmd{Kind: CmdMoveToEnd}, true
case "^":
return Cmd{Kind: CmdMoveToNonBlank}, true
case "|":
return Cmd{
Kind: CmdMoveToColumn,
Num: num,
}, true
// TODO
}
return Cmd{}, false
}
func (p *Parser) ParseInsert(num int, op string) (Cmd, bool) {
switch op {
case "i":
return Cmd{
Kind: CmdInsertBefore,
Num: num,
}, true
case "a":
return Cmd{
Kind: CmdInsertAfter,
Num: num,
}, true
// TODO
}
return Cmd{}, false
}
func (p *Parser) ParseOp(num int, op string) (Cmd, bool) {
switch op {
case "x":
return Cmd{
Kind: CmdOpDelete,
Num: num,
}, true
// TODO
}
return Cmd{}, false
}
func (p *Parser) ParseMisc(op string) (Cmd, bool) {
switch op {
case "ZZ":
return Cmd{Kind: CmdMiscSaveAndQuit}, true
// TODO
}
return Cmd{}, false
}
func (p *Parser) Parse() (Cmd, bool) {
if len(p.buf) < 1 {
return Cmd{}, false
}
if len(p.buf) == 1 {
if p.buf[0] == '0' { // special
return Cmd{Kind: CmdMoveToStart}, true
}
}
i := 0
for i < len(p.buf) {
if p.buf[i] < '0' || p.buf[i] > '9' {
break
}
i++
}
noNum := i == 0
num := 1
if i > 0 {
s := string(p.buf[:i])
n, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
num = n
}
iPrev := i
for i < len(p.buf) {
if p.buf[i] >= '0' && p.buf[i] <= '9' {
break
}
i++
}
if i <= iPrev {
return Cmd{}, false
}
op := string(p.buf[iPrev:i])
cmd, ok := p.ParseMove(noNum, num, op)
if ok {
return cmd, true
}
cmd, ok = p.ParseInsert(num, op)
if ok {
return cmd, true
}
cmd, ok = p.ParseOp(num, op)
if ok {
return cmd, true
}
cmd, ok = p.ParseMisc(op)
if ok {
return cmd, true
}
// TODO
return Cmd{}, false
}

56
internal/editor/run.go Normal file
View File

@@ -0,0 +1,56 @@
package editor
func (ed *Editor) Run(c Cmd) bool {
switch c.Kind {
case CmdMoveLeft:
ed.MoveLeft(c.Num)
return true
case CmdMoveDown:
ed.MoveDown(c.Num)
return true
case CmdMoveUp:
ed.MoveUp(c.Num)
return true
case CmdMoveRight:
ed.MoveRight(c.Num)
return true
case CmdMoveToStart:
ed.MoveToStart()
return true
case CmdMoveToEnd:
ed.MoveToEnd()
return true
case CmdMoveToNonBlank:
ed.MoveToNonBlank()
return true
case CmdMoveToColumn:
ed.MoveToColumn(c.Num)
return true
// TODO
case CmdInsertBefore:
ed.InsertBefore(c.Num)
return true
case CmdInsertAfter:
ed.InsertAfter(c.Num)
return true
// TODO
case CmdOpDelete:
ed.OpDelete(c.Num)
return true
// TODO
case CmdMiscSaveAndQuit:
ed.MiscSaveAndQuit()
return true
// TODO
}
return false
}

View File

@@ -51,7 +51,7 @@ func (ed *Editor) DrawStatus() {
if ed.bell {
termi.EnableInvert()
}
termi.Printf("[%s] %s %d,%d %s", ed.combuf.Cache(), m, ed.row, ed.col, ed.path)
termi.Printf("[%s] %s %d,%d %s", ed.parser.Cache(), m, ed.row, ed.col, ed.path)
if ed.bell {
termi.DisableInvert()
}