From b6066ed3ecc9ed22d07e75ab6ad21ae753b02e7d Mon Sep 17 00:00:00 2001 From: Aki Kareha Date: Fri, 27 Mar 2026 10:34:17 +0900 Subject: [PATCH] Refactor Insert --- internal/editor/command.go | 66 +++++++++++++++++--------- internal/editor/editor.go | 61 ++++++++++++------------ internal/editor/insert.go | 71 ++++++++++++++++++++++++++++ internal/editor/main.go | 97 ++++++++++++++++---------------------- internal/editor/runebuf.go | 29 ++++++++++++ internal/editor/view.go | 38 +++++++-------- 6 files changed, 233 insertions(+), 129 deletions(-) create mode 100644 internal/editor/insert.go create mode 100644 internal/editor/runebuf.go diff --git a/internal/editor/command.go b/internal/editor/command.go index b174b7e..0b2ce82 100644 --- a/internal/editor/command.go +++ b/internal/editor/command.go @@ -1,45 +1,67 @@ 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 +// key: i +func (ed *Editor) Insert() { + if ed.mode == ModeInsert { + panic("invalid state") + } + ed.ins.Enter(ed.lines[ed.row], ed.col) + ed.mode = ModeInsert } -func (ed *Editor) enterInsertAfter() { - rc := ed.runeCount() +// key: a +func (ed *Editor) InsertAfter() { + if ed.mode == ModeInsert { + panic("invalid state") + } + rc := ed.RuneCount() if ed.col >= rc-1 { ed.col = rc - ed.head = ed.lines[ed.row] - ed.tail = "" - ed.mode = modeInsert - return + } else { + ed.MoveRight(1) } - - ed.moveRight(1) - ed.enterInsert() + ed.Insert() } -func (ed *Editor) moveLeft(n int) { +// key: h +func (ed *Editor) MoveLeft(n int) { + if ed.mode != ModeCommand { + panic("invalid state") + } ed.col = max(ed.col-n, 0) } -func (ed *Editor) moveRight(n int) { - ed.col = min(ed.col+n, max(ed.runeCount()-1, 0)) +// key: l +func (ed *Editor) MoveRight(n int) { + if ed.mode != ModeCommand { + panic("invalid state") + } + ed.col = min(ed.col+n, max(ed.RuneCount()-1, 0)) } -func (ed *Editor) moveDown(n int) { +// key: j +func (ed *Editor) MoveDown(n int) { + if ed.mode != ModeCommand { + panic("invalid state") + } ed.row = min(ed.row+n, max(len(ed.lines)-1, 0)) } -func (ed *Editor) moveUp(n int) { +// key: k +func (ed *Editor) MoveUp(n int) { + if ed.mode != ModeCommand { + panic("invalid state") + } ed.row = max(ed.row-n, 0) } -func (ed *Editor) deleteRune(n int) { +// key: x +func (ed *Editor) DeleteRune(n int) { + if ed.mode != ModeCommand { + panic("invalid state") + } if len(ed.lines[ed.row]) < 1 { - ed.ring() + ed.Ring() return } rs := []rune(ed.lines[ed.row]) @@ -50,7 +72,7 @@ func (ed *Editor) deleteRune(n int) { tail := string(rs[ed.col+1:]) ed.lines[ed.row] = head + tail } - rc := ed.runeCount() + rc := ed.RuneCount() if ed.col >= rc { ed.col = max(rc-1, 0) } diff --git a/internal/editor/editor.go b/internal/editor/editor.go index f03e218..4d0a4aa 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -8,26 +8,25 @@ import ( "tea.kareha.org/lab/termi" ) -type mode int +type Mode int const ( - modeCommand mode = iota - modeInsert + ModeCommand Mode = iota + ModeInsert ) type Editor struct { - col, row int - x, y int - vrow int - lines []string - head, tail string - insert *strings.Builder - mode mode - path string - bell bool + col, row int + x, y int + vrow int + lines []string + ins *Insert + mode Mode + path string + bell bool } -func (ed *Editor) load() { +func (ed *Editor) Load() { if ed.path == "" { return } @@ -56,27 +55,25 @@ func Init(args []string) *Editor { } ed := &Editor{ - col: 0, - row: 0, - x: 0, - y: 0, - vrow: 0, - lines: make([]string, 1), - head: "", - tail: "", - insert: new(strings.Builder), - mode: modeCommand, - path: path, - bell: false, + col: 0, + row: 0, + x: 0, + y: 0, + vrow: 0, + lines: make([]string, 1), + ins: NewInsert(), + mode: ModeCommand, + path: path, + bell: false, } - ed.load() + ed.Load() termi.Raw() return ed } -func (ed *Editor) save() { +func (ed *Editor) Save() { if ed.path == "" { return } @@ -93,18 +90,18 @@ func (ed *Editor) Finish() { termi.Cooked() termi.ShowCursor() - ed.save() + ed.Save() } -func (ed *Editor) runeCount() int { +func (ed *Editor) RuneCount() int { return utf8.RuneCountInString(ed.lines[ed.row]) } -func (ed *Editor) insertRune(r rune) { - ed.insert.WriteRune(r) +func (ed *Editor) InsertRune(r rune) { + ed.ins.Write(r) ed.col++ } -func (ed *Editor) ring() { +func (ed *Editor) Ring() { ed.bell = true } diff --git a/internal/editor/insert.go b/internal/editor/insert.go new file mode 100644 index 0000000..82826f0 --- /dev/null +++ b/internal/editor/insert.go @@ -0,0 +1,71 @@ +package editor + +import ( + "unicode/utf8" + + "tea.kareha.org/lab/termi" +) + +type Insert struct { + head, tail string + body *RuneBuf +} + +const maxBodyLen = 1024 + +func NewInsert() *Insert { + return &Insert{ + head: "", + tail: "", + body: new(RuneBuf), + } +} + +func (ins *Insert) Reset() { + ins.head = "" + ins.tail = "" + if ins.body.Len() > maxBodyLen { + ins.body = new(RuneBuf) + } else { + ins.body.Reset() + } +} + +func (ins *Insert) Write(r rune) { + ins.body.WriteRune(r) +} + +func (ins *Insert) Enter(line string, col int) { + rs := []rune(line) + ins.head = string(rs[:col]) + if col < len(rs) { + ins.tail = string(rs[col:]) + } else { + ins.tail = "" + } +} + +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) Width() int { + s := ins.head + ins.body.String() + rc := utf8.RuneCountInString(s) + return termi.StringWidth(s, rc) +} + +func (ins *Insert) Backspace() bool { + return ins.body.Backspace() +} diff --git a/internal/editor/main.go b/internal/editor/main.go index b8507cb..e88d80b 100644 --- a/internal/editor/main.go +++ b/internal/editor/main.go @@ -1,20 +1,17 @@ package editor import ( - "unicode/utf8" - "tea.kareha.org/lab/termi" ) -func (ed *Editor) exitInsert() { - ed.lines[ed.row] = ed.head + ed.insert.String() + ed.tail - ed.tail = "" - ed.insert.Reset() - ed.mode = modeCommand - ed.moveLeft(1) +func (ed *Editor) ExitInsert() { + ed.lines[ed.row] = ed.ins.Line() + ed.ins.Reset() + ed.mode = ModeCommand + ed.MoveLeft(1) } -func (ed *Editor) insertNewline() { +func (ed *Editor) InsertNewline() { before := make([]string, 0, len(ed.lines)+1) before = append(before, ed.lines[:ed.row]...) var after []string @@ -23,101 +20,89 @@ func (ed *Editor) insertNewline() { } else { after = []string{} } - newLines := []string{ - ed.head + ed.insert.String(), - ed.tail, - } - ed.lines = append(append(before, newLines...), after...) - + lines := ed.ins.Newline() + ed.lines = append(append(before, lines...), after...) ed.row++ - ed.col = 0 - ed.head = "" - ed.insert.Reset() } -func (ed *Editor) deleteBefore() { - if ed.insert.Len() < 1 { - ed.ring() +func (ed *Editor) DeleteBefore() { + if !ed.ins.Backspace() { + ed.Ring() return } - insert := ed.insert.String() - _, size := utf8.DecodeLastRuneInString(insert) - insert = insert[:len(insert)-size] - ed.insert.Reset() - ed.insert.WriteString(insert) ed.col-- } func (ed *Editor) Main() { for { - ed.repaint() + ed.Repaint() key := termi.ReadKey() switch ed.mode { - case modeCommand: + case ModeCommand: switch key.Kind { case termi.KeyRune: switch key.Rune { case 'q': return case 'i': - ed.enterInsert() + ed.Insert() case 'a': - ed.enterInsertAfter() + ed.InsertAfter() case 'h': - ed.moveLeft(1) + ed.MoveLeft(1) case 'l': - ed.moveRight(1) + ed.MoveRight(1) case 'j': - ed.moveDown(1) + ed.MoveDown(1) case 'k': - ed.moveUp(1) + ed.MoveUp(1) case 'x': - ed.deleteRune(1) + ed.DeleteRune(1) default: - ed.ring() + ed.Ring() } case termi.KeyUp: - ed.moveUp(1) + ed.MoveUp(1) case termi.KeyDown: - ed.moveDown(1) + ed.MoveDown(1) case termi.KeyRight: - ed.moveRight(1) + ed.MoveRight(1) case termi.KeyLeft: - ed.moveLeft(1) + ed.MoveLeft(1) default: - ed.ring() + ed.Ring() } - case modeInsert: + case ModeInsert: switch key.Kind { case termi.KeyRune: switch key.Rune { case termi.RuneEscape: - ed.exitInsert() + ed.ExitInsert() case termi.RuneEnter: - ed.insertNewline() + ed.InsertNewline() case termi.RuneBackspace: - ed.deleteBefore() + ed.DeleteBefore() case termi.RuneDelete: - ed.deleteBefore() + ed.DeleteBefore() default: - ed.insertRune(key.Rune) + ed.InsertRune(key.Rune) } case termi.KeyUp: - ed.exitInsert() - ed.moveUp(1) + ed.ExitInsert() + ed.MoveUp(1) case termi.KeyDown: - ed.exitInsert() - ed.moveDown(1) + ed.ExitInsert() + ed.MoveDown(1) case termi.KeyRight: - ed.exitInsert() - ed.moveRight(1) + ed.ExitInsert() + ed.MoveRight(1) case termi.KeyLeft: - ed.exitInsert() - ed.moveLeft(1) + ed.ExitInsert() + ed.MoveLeft(1) default: - ed.ring() + ed.Ring() } } } diff --git a/internal/editor/runebuf.go b/internal/editor/runebuf.go new file mode 100644 index 0000000..5e91b9d --- /dev/null +++ b/internal/editor/runebuf.go @@ -0,0 +1,29 @@ +package editor + +type RuneBuf struct { + buf []rune +} + +func (b *RuneBuf) WriteRune(r rune) { + b.buf = append(b.buf, r) +} + +func (b *RuneBuf) Backspace() bool { + if len(b.buf) == 0 { + return false + } + b.buf = b.buf[:len(b.buf)-1] + return true +} + +func (b *RuneBuf) String() string { + return string(b.buf) +} + +func (b *RuneBuf) Reset() { + b.buf = b.buf[:0] +} + +func (b *RuneBuf) Len() int { + return len(b.buf) +} diff --git a/internal/editor/view.go b/internal/editor/view.go index a7e4f2c..598d7bc 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -6,21 +6,21 @@ import ( "tea.kareha.org/lab/termi" ) -func (ed *Editor) lineHeight(line string) int { +func (ed *Editor) LineHeight(line string) int { w, _ := termi.Size() rc := utf8.RuneCountInString(line) width := termi.StringWidth(line, rc) return 1 + max(width-1, 0)/w } -func (ed *Editor) drawBuffer() { +func (ed *Editor) DrawBuffer() { _, h := termi.Size() y := 0 for i := ed.vrow; i < len(ed.lines); i++ { var line string - if ed.mode == modeInsert && i == ed.row { - line = ed.head + ed.insert.String() + ed.tail + if ed.mode == ModeInsert && i == ed.row { + line = ed.ins.Line() } else { line = ed.lines[i] } @@ -28,7 +28,7 @@ func (ed *Editor) drawBuffer() { termi.MoveCursor(0, y) termi.Draw(line) - y += ed.lineHeight(line) + y += ed.LineHeight(line) if y >= h-1 { break } @@ -40,12 +40,12 @@ func (ed *Editor) drawBuffer() { } } -func (ed *Editor) drawStatus() { +func (ed *Editor) DrawStatus() { var m string switch ed.mode { - case modeCommand: + case ModeCommand: m = "c" - case modeInsert: + case ModeInsert: m = "i" } @@ -61,23 +61,23 @@ func (ed *Editor) drawStatus() { ed.bell = false } -func (ed *Editor) updateCursor() { +func (ed *Editor) UpdateCursor() { w, h := termi.Size() var dy int switch ed.mode { - case modeCommand: + case ModeCommand: ed.row = min(max(ed.row, 0), max(len(ed.lines)-1, 0)) - len := ed.runeCount() + len := ed.RuneCount() ed.col = min(ed.col, max(len-1, 0)) // XXX approximation width := termi.StringWidth(ed.lines[ed.row], ed.col) ed.x = width % w dy = width / w - case modeInsert: + case ModeInsert: // XXX approximation - width := termi.StringWidth(ed.head+ed.insert.String(), ed.col) + width := ed.ins.Width() ed.x = width % w dy = width / w } @@ -88,7 +88,7 @@ func (ed *Editor) updateCursor() { y := 0 for i := ed.vrow; i < ed.row; i++ { - y += ed.lineHeight(ed.lines[i]) + y += ed.LineHeight(ed.lines[i]) } ed.y = y + dy @@ -97,22 +97,22 @@ func (ed *Editor) updateCursor() { y := 0 for i := ed.vrow; i < ed.row; i++ { - y += ed.lineHeight(ed.lines[i]) + y += ed.LineHeight(ed.lines[i]) } ed.y = y + dy } } -func (ed *Editor) repaint() { +func (ed *Editor) Repaint() { termi.HideCursor() termi.Clear() termi.HomeCursor() - ed.updateCursor() + ed.UpdateCursor() - ed.drawBuffer() - ed.drawStatus() + ed.DrawBuffer() + ed.DrawStatus() termi.MoveCursor(ed.x, ed.y)