From c7447d6f136092106cb7fbc23302c973cdb67bd8 Mon Sep 17 00:00:00 2001 From: Aki Kareha Date: Wed, 25 Mar 2026 17:11:08 +0900 Subject: [PATCH] Add multiline edit --- cmd/levi/main.go | 17 ++----- internal/editor/command.go | 38 +++++++++++++++ internal/editor/editor.go | 94 +++++++++++++++++++++---------------- internal/editor/keyboard.go | 3 +- internal/util/util.go | 4 +- 5 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 internal/editor/command.go diff --git a/cmd/levi/main.go b/cmd/levi/main.go index edb20df..978ba2d 100644 --- a/cmd/levi/main.go +++ b/cmd/levi/main.go @@ -1,22 +1,11 @@ package main import ( - "tea.kareha.org/lab/levi/internal/console" "tea.kareha.org/lab/levi/internal/editor" ) func main() { - // init - console.Raw() - defer func() { - console.Cooked() - console.ShowCursor() - }() - - // main - editor.New().Main() - - // cleanup - console.Clear() - console.HomeCursor() + ed := editor.Init() + defer ed.Finish() + ed.Main() } diff --git a/internal/editor/command.go b/internal/editor/command.go new file mode 100644 index 0000000..4bafc5e --- /dev/null +++ b/internal/editor/command.go @@ -0,0 +1,38 @@ +package editor + +func (ed *Editor) enterInsert() { + rs := []rune(ed.lines[ed.row]) + ed.head = string(rs[:ed.col]) + ed.tail = string(rs[ed.col:]) + ed.mode = modeInsert +} + +func (ed *Editor) enterInsertAfter() { + len := ed.runeCount() + if ed.col >= len-1 { + ed.col = len + ed.head = ed.lines[ed.row] + ed.tail = "" + ed.mode = modeInsert + return + } + + ed.moveRight(1) + ed.enterInsert() +} + +func (ed *Editor) moveLeft(n int) { + ed.col = max(ed.col-n, 0) +} + +func (ed *Editor) moveRight(n int) { + ed.col = min(ed.col+n, max(ed.runeCount()-1, 0)) +} + +func (ed *Editor) moveDown(n int) { + ed.row = min(ed.row+n, max(len(ed.lines)-1, 0)) +} + +func (ed *Editor) moveUp(n int) { + ed.row = max(ed.row-n, 0) +} diff --git a/internal/editor/editor.go b/internal/editor/editor.go index af2d018..55dff36 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -20,12 +20,15 @@ type Editor struct { kb *Keyboard col, row int x, y int + lines []string head, tail string insert *strings.Builder mode mode } -func New() *Editor { +func Init() *Editor { + console.Raw() + scr := NewScreen() kb := NewKeyboard() @@ -36,6 +39,7 @@ func New() *Editor { row: 0, x: 0, y: 0, + lines: make([]string, 1), head: "", tail: "", insert: new(strings.Builder), @@ -43,18 +47,27 @@ func New() *Editor { } } -func (ed *Editor) addRune(r rune) { - ed.insert.WriteRune(r) +func (ed *Editor) Finish() { + console.Clear() + console.HomeCursor() + console.Cooked() + console.ShowCursor() +} + +func (ed *Editor) runeCount() int { + return utf8.RuneCountInString(ed.lines[ed.row]) } func (ed *Editor) drawBuffer() { - switch ed.mode { - case modeCommand: - console.Print(ed.head) - case modeInsert: - console.Print(ed.head) - console.Print(ed.insert.String()) - console.Print(ed.tail) + for i := 0; i < len(ed.lines); i++ { + console.MoveCursor(0, i) + if ed.mode == modeInsert && i == ed.row { + console.Print(ed.head) + console.Print(ed.insert.String()) + console.Print(ed.tail) + } else { + console.Print(ed.lines[i]) + } } } @@ -73,7 +86,10 @@ func (ed *Editor) drawStatus() { func (ed *Editor) updateCursor() { switch ed.mode { case modeCommand: - ed.x = util.StringWidth(ed.head, ed.col) + ed.row = min(max(ed.row, 0), max(len(ed.lines)-1, 0)) + len := ed.runeCount() + ed.col = min(ed.col, max(len-1, 0)) + ed.x = util.StringWidth(ed.lines[ed.row], ed.col) case modeInsert: ed.x = util.StringWidth(ed.head+ed.insert.String(), ed.col) } @@ -95,31 +111,28 @@ func (ed *Editor) repaint() { console.ShowCursor() } -func (ed *Editor) enterInsert() { - rs := []rune(ed.head) - ed.head = string(rs[:ed.col]) - ed.tail = string(rs[ed.col:]) - ed.mode = modeInsert +func (ed *Editor) exitInsert() { + ed.lines[ed.row] = ed.head + ed.insert.String() + ed.tail + ed.tail = "" + ed.insert = new(strings.Builder) + ed.mode = modeCommand + ed.moveLeft(1) } -func (ed *Editor) enterInsertAfter() { - len := utf8.RuneCountInString(ed.head) - if ed.col < len-1 { - ed.moveRight(1) - ed.enterInsert() - return - } else { - ed.col++ - ed.mode = modeInsert - } +func (ed *Editor) insertNewline() { + ed.lines[ed.row] = ed.head + ed.insert.String() + ed.lines = append(ed.lines, "") + copy(ed.lines[ed.row+1:], ed.lines[ed.row:]) + ed.row++ + ed.lines[ed.row] = ed.tail + ed.col = 0 + ed.head = "" + ed.insert = new(strings.Builder) } -func (ed *Editor) moveLeft(n int) { - ed.col = max(ed.col-n, 0) -} - -func (ed *Editor) moveRight(n int) { - ed.col = min(ed.col+n, max(utf8.RuneCountInString(ed.head)-1, 0)) +func (ed *Editor) insertRune(r rune) { + ed.insert.WriteRune(r) + ed.col++ } func (ed *Editor) Main() { @@ -140,18 +153,19 @@ func (ed *Editor) Main() { ed.moveLeft(1) case 'l': ed.moveRight(1) + case 'j': + ed.moveDown(1) + case 'k': + ed.moveUp(1) } case modeInsert: switch r { - case Esc: - ed.head = ed.head + ed.insert.String() + ed.tail - ed.tail = "" - ed.insert = new(strings.Builder) - ed.mode = modeCommand - ed.moveLeft(1) + case Escape: + ed.exitInsert() + case Enter: + ed.insertNewline() default: - ed.addRune(r) - ed.col++ + ed.insertRune(r) } } } diff --git a/internal/editor/keyboard.go b/internal/editor/keyboard.go index 440878d..73f75f7 100644 --- a/internal/editor/keyboard.go +++ b/internal/editor/keyboard.go @@ -4,7 +4,8 @@ import ( "tea.kareha.org/lab/levi/internal/console" ) -const Esc rune = 0x1b +const Escape rune = 0x1b +const Enter rune = '\r' type Keyboard struct{} diff --git a/internal/util/util.go b/internal/util/util.go index f7727b6..569dd7b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -20,7 +20,7 @@ func isEmoji(r rune) bool { return r >= 0x1f300 && r <= 0x1faff } -func RuneWidth(r rune) int { +func runeWidth(r rune) int { // control code if r == 0 { return 0 @@ -54,7 +54,7 @@ func StringWidth(s string, col int) int { if i >= col { break } - sum += RuneWidth(r) + sum += runeWidth(r) i++ } return sum