From 52e974a721dfc0c970e2ce356791aaa20b364def Mon Sep 17 00:00:00 2001 From: Aki Kareha Date: Fri, 27 Mar 2026 23:50:04 +0900 Subject: [PATCH] Add parser --- internal/editor/cmd.go | 147 ++++++++++++++++++++++++++++ internal/editor/combuf.go | 41 -------- internal/editor/command.go | 49 ---------- internal/editor/editor.go | 14 +-- internal/editor/input.go | 69 +++++++++++++ internal/editor/insert.go | 77 ++++----------- internal/editor/main.go | 130 ++----------------------- internal/editor/misc.go | 6 ++ internal/editor/op.go | 21 ++++ internal/editor/parser.go | 192 +++++++++++++++++++++++++++++++++++++ internal/editor/run.go | 56 +++++++++++ internal/editor/view.go | 2 +- 12 files changed, 525 insertions(+), 279 deletions(-) create mode 100644 internal/editor/cmd.go delete mode 100644 internal/editor/combuf.go delete mode 100644 internal/editor/command.go create mode 100644 internal/editor/input.go create mode 100644 internal/editor/misc.go create mode 100644 internal/editor/op.go create mode 100644 internal/editor/parser.go create mode 100644 internal/editor/run.go diff --git a/internal/editor/cmd.go b/internal/editor/cmd.go new file mode 100644 index 0000000..78a73ca --- /dev/null +++ b/internal/editor/cmd.go @@ -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 +} diff --git a/internal/editor/combuf.go b/internal/editor/combuf.go deleted file mode 100644 index 6c52de6..0000000 --- a/internal/editor/combuf.go +++ /dev/null @@ -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 = "" -} diff --git a/internal/editor/command.go b/internal/editor/command.go deleted file mode 100644 index 1404b56..0000000 --- a/internal/editor/command.go +++ /dev/null @@ -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 -} diff --git a/internal/editor/editor.go b/internal/editor/editor.go index 3611a5a..92ccc5f 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -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() { diff --git a/internal/editor/input.go b/internal/editor/input.go new file mode 100644 index 0000000..e4aef53 --- /dev/null +++ b/internal/editor/input.go @@ -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() +} diff --git a/internal/editor/insert.go b/internal/editor/insert.go index eb160fa..aa976b4 100644 --- a/internal/editor/insert.go +++ b/internal/editor/insert.go @@ -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) } diff --git a/internal/editor/main.go b/internal/editor/main.go index 4e390cb..d6e0e0a 100644 --- a/internal/editor/main.go +++ b/internal/editor/main.go @@ -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) diff --git a/internal/editor/misc.go b/internal/editor/misc.go new file mode 100644 index 0000000..7d83892 --- /dev/null +++ b/internal/editor/misc.go @@ -0,0 +1,6 @@ +package editor + +// ZZ : Save and quit. +func (ed *Editor) MiscSaveAndQuit() { + ed.quit = true +} diff --git a/internal/editor/op.go b/internal/editor/op.go new file mode 100644 index 0000000..c4689e3 --- /dev/null +++ b/internal/editor/op.go @@ -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() +} diff --git a/internal/editor/parser.go b/internal/editor/parser.go new file mode 100644 index 0000000..cb3b8b7 --- /dev/null +++ b/internal/editor/parser.go @@ -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 +} diff --git a/internal/editor/run.go b/internal/editor/run.go new file mode 100644 index 0000000..98726cd --- /dev/null +++ b/internal/editor/run.go @@ -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 +} diff --git a/internal/editor/view.go b/internal/editor/view.go index 422baf8..2ec1b84 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -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() }