Add dirty command parser from micro vi plugin

This commit is contained in:
2026-03-27 21:18:16 +09:00
parent 0b51c76132
commit dbba404268
7 changed files with 505 additions and 75 deletions

223
commands.txt Normal file
View File

@@ -0,0 +1,223 @@
= Planned Commands for levi =
Implementation status:
* (Buggy): Already implemented, but has some bugs.
* (Partially): Partially implemented.
* (Planned): Not implemented yet, but planned.
* (Stalled): Tried to implement, but not functional yet.
* (Out of Scope): Not planned to implement.
Command categories:
* Motion
* Marking
* View
* Search
* Character Finding
* Insertion
* Operator (Copy / Delte / Change)
* Editing
* Miscellaneous
* Prompt
== Motion Commands ==
=== Move by Character / Move by Line ===
* h : Move cursor left by character. (MoveLeft)
* j : Move cursor down by line. (MoveDown)
* k : Move cursor up by line. (MoveUp)
* l : Move cursor right by character. (MoveRight)
=== Move in Line ===
* 0 : Move cursor to start of current line. (MoveToStart)
* $ : Move cursor to end of current line. (MoveToEnd)
* ^ : Move cursor to first non-blank character of current line. (MoveToNonBlank)
* <num>| : Move cursor to column <num> of current line. (MoveToColumn)
(Note: Proper vi's column number is visual-based, but levi' is rune-based.)
=== Move by Word / Move by Loose Word ===
* w : Move cursor forward by word. (MoveByWord)
* b : Move cursor backward by word. (MoveBackwardByWord)
* e : Move cursor to end of word. (MoveToEndOfWord)
* W : Move cursor forward by loose word. (MoveByLooseWord)
* B : Move cursor backward by loose word. (MoveBackwardByLooseWord)
* E : Move cursor to end of loose word. (MoveToEndOfLooseWord)
=== Move by Line ===
* Enter, + : Move cursor to first non-blank character of next line. (MoveToNonBlankOfNextLine)
* - : Move cursor to first non-blank character of previous line. (MoveToNonBlankOfPrevLine)
* G : Move cursor to last line. (MoveToLastLine)
* <num>G : Move cursor to line <num>. (MoveToLine)
=== Move by Block ===
* ) : Move cursor forward by sentence. (MoveBySentence)
* ( : Move cursor backward by sentence. (MoveBackwardBySentence)
* } : Move cursor forward by paragraph. (MoveByParagraph)
* { : Move cursor backward by paragraph. (MoveBackwardByParagraph)
(Note: Proper vi respects nroff/troff directives, but levi doesn't.)
* ]] : Move cursor forward by section. (MoveBySection)
(Note: Proper vi respects nroff/troff directives, but levi doesn't.)
* [[ : Move cursor backward by section. (MoveBackwardBySection)
(Note: Proper vi respects nroff/troff directives, but levi doesn't.)
=== Move in View ===
* H : Move cursor to top of view. (MoveToTopOfView)
* M : Move cursor to middle of view. (MoveToMiddleOfView)
* L : Move cursor to bottom of view. (MoveToBottomOfView)
* <num>H : Move cursor below <num> lines from top of view. (MoveToBelowTopOfView)
* <num>L : Move cursor above <num> lines from bottom of view. (MoveToAboveBottomOfView)
== Marking Commands ==
=== Set Mark / Move to Mark ===
* m<letter> : Mark current cursor position labelled by <letter>. (MarkSet)
* `<letter> : Move cursor to marked position labelled by <letter>. (MarkMoveTo)
* '<letter> : Move cursor to marked line labelled by <letter>. (MarkMoveToLine)
=== Move by Context ===
* `` : Move cursor to previous position in context. (MarkBack)
* '' : Move cursor to previous line in context. (MarkBackToLine)
== View Commands ==
=== Scroll by View Height / Scroll by Line ===
* Ctrl-f : Scroll down by view height. (ViewDown)
* Ctrl-b : Scroll up by view height. (ViewUp)
* Ctrl-d : Scroll down by half view height. (ViewDownHalf)
* Ctrl-u : Scroll up by half view height. (ViewUpHalf)
* Ctrl-y : Scroll down by line. (ViewDownLine)
* Ctrl-e : Scroll up by line. (ViewUpLine)
=== Reposition ===
* z Enter : Reposition cursor line to top of view. (ViewToTop)
* z. : Reposition cursor line middle of view. (ViewToMiddle)
* z- : Reposition cursor line bottom of view. (ViewToBottom)
=== Redraw ===
* Ctrl-l : Redraw view. (ViewRedraw)
== Search Commands ==
* /<pattern> Enter : Search <pattern> forward. (SearchForward)
* ?<pattern> Enter : Search <pattern> backward. (SearchBackward)
* n : Search next match. (SearchNextMatch)
* N : Search previous match. (SearchPrevMatch)
* / Enter : Repeat last search forward. (SearchRepeatForward)
* ? Enter : Repeat last search backward. (SearchRepeatBackward)
== Character Finding Commands ==
* f<letter> : Find character <letter> forward in current line. (FindForward)
* F<letter> : Find character <letter> backward in current line. (FindBackward)
* t<letter> : Find before character <letter> forward in current line. (FindBeforeForward)
* T<letter> : Find before character <letter> backward in current line. (FindBeforeBackward)
* ; : Find next match. (FindNextMatch)
* , : Find previous match. (FindPrevMatch)
== Insertion Commands ==
=== Enter Insert Mode ===
* i : Switch to insert mode before cursor. (InsertBefore)
* a : Switch to insert mode after cursor. (InsertAfter)
* I : Switch to insert mode before first non-blank character of current line. (InsertBeforeNonBlank)
* A : Switch to insert mode after end of current line. (InsertAfterEnd)
* R : Switch to replace (overwrite) mode. (InsertOverwrite)
=== Open Line ===
* o : Open a new line below and switch to insert mode. (InsertOpenBelow)
* O : Open a new line above and switch to insert mode. (InsertOpenAbove)
== Operator Commands (Copy / Delte / Change) ==
=== Copy (Yank) ===
* yy, Y : Copy current line. (OpCopyLine)
* y<mv> : Copy region from current cursor to destination of motion <mv>. (OpCopyRegion, OpCopyLineRegion)
* yw : Copy word. (OpCopyWord)
* y$ : Copy to end of current line. (OpCopyToEnd)
* "<reg>yy : Copy current line into register <reg>. (OpCopyLineIntoReg)
=== Paste (Put) ===
* p : Paste after cursor. (OpPaste)
* P : Paste before cursor. (OpPasteBefore)
* "<reg>p : Paste from register <reg>. (OpPasteFromReg)
=== Delete ===
* x : Delete character under cursor. (OpDelete)
* X : Delete character before cursor. (OpDeleteBefore)
* dd : Delete current line. (OpDeleteLine)
* d<mv> : Delete region from current cursor to destination of motion <mv>. (OpDeleteRegion, OpDeleteLineRegion)
* dw : Delete word. (OpDeleteWord)
* d$, D : Delete to end of current line. (OpDeleteToEnd)
=== Change / Substitute ===
* cc : Change current line. (OpChangeLine)
* c<mv> : Change region from current cursor to destination of motion <mv>. (OpChangeRegion, OpChangeLineRegion)
* cw : Change word. (OpChangeWord)
* C : Change to end of current line. (OpChangeToEnd)
* s : Substitute one character under cursor. (OpSubst)
* S : Substtute current line (equals cc). (OpSubstLine)
== Editing Commands ==
* r : Replace single character under cursor. (EditReplace)
* J : Join current line with next line. (EditJoin)
* >> : Indent current line. (EditIndent)
* << : Outdent current line. (EditOutdent)
* > <mv> : Indent region from current cursor to destination of motion <mv>. (EditIndentRegion)
* < <mv> : Outdent region from current cursor to destination of motion <mv>. (EditOutdentRegion)
== Miscellaneous Commands ==
* Ctrl-g : Show info such as current cursor position. (MiscShowInfo)
* . : Repeat last edit. (MiscRepeat)
* u : Undo. (MiscUndo)
* U : Restore current line to previous state. (MiscRestore)
* ZZ : Save and quit. (MiscSaveAndQuit)
== Prompt Commands ==
=== Move ===
* :<num> Enter : Move cursor to line <num>. (PromptMoveToLine)
=== File ===
* :wq Enter : Save current file and quit. (PromptSaveAndQuit)
* :w Enter : Save current file. (PromptSave)
* :w! Enter : Force save current file. (PromptForceSave)
* :q Enter : Quit editor. (PromptQuit)
* :q! Enter : Force quit editor. (PromptForceQuit)
* :e Enter : Open file. (PromptOpen)
* :e! Enter : Force open file. (PromptForceOpen)
* :r Enter : Read file and insert to current buffer. (PromptRead)
* :n Enter : Switch to next buffer (tab). (PromptNext)
* :prev Enter : Switch to previous buffer (tab). (PromptPrev) (extension)
=== Utility ===
* :sh Enter : Execute shell. (PromptShell)
(Note: Only Unix-like OSes are supported.)
=== From Vim ===
* :wa Enter : Save all files. (PromptSaveAll)
* :qa Enter : Close all files and quit editor. (PromptQuitAll)
* :qa! Enter : Force close all files and quit editor. (PromptForceQuitAll)

41
internal/editor/combuf.go Normal file
View File

@@ -0,0 +1,41 @@
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,7 +1,7 @@
package editor
// key: i
func (ed *Editor) Insert() {
func (ed *Editor) InsertBefore() {
if ed.mode == ModeInsert {
panic("invalid state")
}
@@ -20,47 +20,11 @@ func (ed *Editor) InsertAfter() {
} else {
ed.MoveRight(1)
}
ed.Insert()
}
// key: h
func (ed *Editor) MoveLeft(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col -= n
ed.Confine()
}
// key: l
func (ed *Editor) MoveRight(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col += n
ed.Confine()
}
// key: j
func (ed *Editor) MoveDown(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.row += n
ed.Confine()
}
// key: k
func (ed *Editor) MoveUp(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.row -= n
ed.Confine()
ed.InsertBefore()
}
// key: x
func (ed *Editor) DeleteRune(n int) {
func (ed *Editor) OpDelete(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
@@ -78,3 +42,8 @@ func (ed *Editor) DeleteRune(n int) {
}
ed.Confine()
}
// key: ZZ
func (ed *Editor) MiscSaveAndQuit() {
ed.quit = true
}

View File

@@ -13,6 +13,8 @@ type Mode int
const (
ModeCommand Mode = iota
ModeInsert
ModeSearch
ModePrompt
)
type Editor struct {
@@ -25,6 +27,8 @@ type Editor struct {
mode Mode
path string
bell bool
combuf *Combuf
quit bool
}
func (ed *Editor) Load(path string) {
@@ -51,18 +55,20 @@ func Init(args []string) *Editor {
w, h := termi.Size()
ed := &Editor{
col: 0,
row: 0,
vrow: 0,
w: w,
h: h,
x: 0,
y: 0,
lines: make([]string, 1),
ins: NewInsert(),
mode: ModeCommand,
path: path,
bell: false,
col: 0,
row: 0,
vrow: 0,
w: w,
h: h,
x: 0,
y: 0,
lines: make([]string, 1),
ins: NewInsert(),
mode: ModeCommand,
path: path,
bell: false,
combuf: NewCombuf(),
quit: false,
}
if path != "" {

View File

@@ -1,6 +1,9 @@
package editor
import (
"regexp"
"strconv"
"tea.kareha.org/lab/termi"
)
@@ -30,7 +33,7 @@ func (ed *Editor) InsertNewline() {
ed.lines = append(append(before, lines...), after...)
ed.row++
ed.col = 0
// row and col are confined automatically
// row and col are already confined
}
func (ed *Editor) Backspace() {
@@ -42,11 +45,64 @@ func (ed *Editor) Backspace() {
return
}
ed.col--
// col is confined automatically
// 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 {
for !ed.quit {
ed.Draw()
key := termi.ReadKey()
@@ -54,25 +110,72 @@ func (ed *Editor) Main() {
case ModeCommand:
switch key.Kind {
case termi.KeyRune:
switch key.Rune {
case 'q':
return
case 'i':
ed.Insert()
case 'a':
ed.InsertAfter()
case 'h':
ed.MoveLeft(1)
case 'l':
ed.MoveRight(1)
case 'j':
ed.MoveDown(1)
case 'k':
ed.MoveUp(1)
case 'x':
ed.DeleteRune(1)
default:
if key.Rune == termi.RuneEscape {
ed.combuf.ClearAll()
ed.Ring()
continue
}
ed.combuf.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
}
*/
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)

82
internal/editor/move.go Normal file
View File

@@ -0,0 +1,82 @@
package editor
// h : Move cursor left by character.
func (ed *Editor) MoveLeft(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col -= n
ed.Confine()
}
// j : Move cursor down by line.
func (ed *Editor) MoveDown(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.row += n
ed.Confine()
}
// k : Move cursor up by line.
func (ed *Editor) MoveUp(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.row -= n
ed.Confine()
}
// l : Move cursor right by character.
func (ed *Editor) MoveRight(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col += n
ed.Confine()
}
// 0 : Move cursor to start of current line.
func (ed *Editor) MoveToStart() {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col = 0
// col is already confined
}
// $ : Move cursor to end of current line.
func (ed *Editor) MoveToEnd() {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col = ed.RuneCount() - 1
ed.Confine()
}
// ^ : Move cursor to first non-blank character of current line.
func (ed *Editor) MoveToNonBlank() {
if ed.mode != ModeCommand {
panic("invalid state")
}
line := ed.CurrentLine()
i := 0
for _, r := range line {
if r != ' ' && r != '\t' {
break
}
i++
}
ed.col = i
ed.Confine()
}
// <num>| : Move cursor to column <num> of current line.
// (Note: Proper vi's column number is visual-based, but levi' is rune-based.)
func (ed *Editor) MoveToColumn(n int) {
if ed.mode != ModeCommand {
panic("invalid state")
}
ed.col = n - 1
ed.Confine()
}

View File

@@ -36,16 +36,22 @@ func (ed *Editor) DrawStatus() {
var m string
switch ed.mode {
case ModeCommand:
m = "c"
m = "command"
case ModeInsert:
m = "i"
m = "insert"
case ModeSearch:
m = "search"
case ModePrompt:
m = "prompt"
default:
panic("invalid mode")
}
termi.MoveCursor(0, ed.h-1)
if ed.bell {
termi.EnableInvert()
}
termi.Printf("%s %d,%d %s", m, ed.row, ed.col, ed.path)
termi.Printf("[%s] %s %d,%d %s", ed.combuf.Cache(), m, ed.row, ed.col, ed.path)
if ed.bell {
termi.DisableInvert()
}