diff --git a/commands.txt b/commands.txt new file mode 100644 index 0000000..f02a94a --- /dev/null +++ b/commands.txt @@ -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) +* | : Move cursor to column 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) +* G : Move cursor to line . (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) +* H : Move cursor below lines from top of view. (MoveToBelowTopOfView) +* L : Move cursor above lines from bottom of view. (MoveToAboveBottomOfView) + +== Marking Commands == + +=== Set Mark / Move to Mark === + +* m : Mark current cursor position labelled by . (MarkSet) +* ` : Move cursor to marked position labelled by . (MarkMoveTo) +* ' : Move cursor to marked line labelled by . (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 == + +* / Enter : Search forward. (SearchForward) +* ? Enter : Search 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 : Find character forward in current line. (FindForward) +* F : Find character backward in current line. (FindBackward) +* t : Find before character forward in current line. (FindBeforeForward) +* T : Find before character 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 : Copy region from current cursor to destination of motion . (OpCopyRegion, OpCopyLineRegion) +* yw : Copy word. (OpCopyWord) +* y$ : Copy to end of current line. (OpCopyToEnd) +* "yy : Copy current line into register . (OpCopyLineIntoReg) + +=== Paste (Put) === + +* p : Paste after cursor. (OpPaste) +* P : Paste before cursor. (OpPasteBefore) +* "p : Paste from register . (OpPasteFromReg) + +=== Delete === + +* x : Delete character under cursor. (OpDelete) +* X : Delete character before cursor. (OpDeleteBefore) +* dd : Delete current line. (OpDeleteLine) +* d : Delete region from current cursor to destination of motion . (OpDeleteRegion, OpDeleteLineRegion) +* dw : Delete word. (OpDeleteWord) +* d$, D : Delete to end of current line. (OpDeleteToEnd) + +=== Change / Substitute === + +* cc : Change current line. (OpChangeLine) +* c : Change region from current cursor to destination of motion . (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) +* > : Indent region from current cursor to destination of motion . (EditIndentRegion) +* < : Outdent region from current cursor to destination of motion . (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 === + +* : Enter : Move cursor to line . (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) diff --git a/internal/editor/combuf.go b/internal/editor/combuf.go new file mode 100644 index 0000000..6c52de6 --- /dev/null +++ b/internal/editor/combuf.go @@ -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 = "" +} diff --git a/internal/editor/command.go b/internal/editor/command.go index 74dd862..1404b56 100644 --- a/internal/editor/command.go +++ b/internal/editor/command.go @@ -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 +} diff --git a/internal/editor/editor.go b/internal/editor/editor.go index 33794df..3611a5a 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -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 != "" { diff --git a/internal/editor/main.go b/internal/editor/main.go index 485088e..4e390cb 100644 --- a/internal/editor/main.go +++ b/internal/editor/main.go @@ -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) diff --git a/internal/editor/move.go b/internal/editor/move.go new file mode 100644 index 0000000..52c50f0 --- /dev/null +++ b/internal/editor/move.go @@ -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() +} + +// | : Move cursor to column 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() +} diff --git a/internal/editor/view.go b/internal/editor/view.go index e6b877c..422baf8 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -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() }