diff --git a/internal/editor/command.go b/internal/editor/command.go index 0b2ce82..74dd862 100644 --- a/internal/editor/command.go +++ b/internal/editor/command.go @@ -5,7 +5,7 @@ func (ed *Editor) Insert() { if ed.mode == ModeInsert { panic("invalid state") } - ed.ins.Enter(ed.lines[ed.row], ed.col) + ed.ins.Init(ed.CurrentLine(), ed.col) ed.mode = ModeInsert } @@ -28,7 +28,8 @@ func (ed *Editor) MoveLeft(n int) { if ed.mode != ModeCommand { panic("invalid state") } - ed.col = max(ed.col-n, 0) + ed.col -= n + ed.Confine() } // key: l @@ -36,7 +37,8 @@ func (ed *Editor) MoveRight(n int) { if ed.mode != ModeCommand { panic("invalid state") } - ed.col = min(ed.col+n, max(ed.RuneCount()-1, 0)) + ed.col += n + ed.Confine() } // key: j @@ -44,7 +46,8 @@ 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)) + ed.row += n + ed.Confine() } // key: k @@ -52,7 +55,8 @@ func (ed *Editor) MoveUp(n int) { if ed.mode != ModeCommand { panic("invalid state") } - ed.row = max(ed.row-n, 0) + ed.row -= n + ed.Confine() } // key: x @@ -60,11 +64,11 @@ func (ed *Editor) DeleteRune(n int) { if ed.mode != ModeCommand { panic("invalid state") } - if len(ed.lines[ed.row]) < 1 { + if len(ed.CurrentLine()) < 1 { ed.Ring() return } - rs := []rune(ed.lines[ed.row]) + rs := []rune(ed.CurrentLine()) if ed.col < 1 { ed.lines[ed.row] = string(rs[1:]) } else { @@ -72,8 +76,5 @@ func (ed *Editor) DeleteRune(n int) { tail := string(rs[ed.col+1:]) ed.lines[ed.row] = head + tail } - rc := ed.RuneCount() - if ed.col >= rc { - ed.col = max(rc-1, 0) - } + ed.Confine() } diff --git a/internal/editor/editor.go b/internal/editor/editor.go index 4d0a4aa..33794df 100644 --- a/internal/editor/editor.go +++ b/internal/editor/editor.go @@ -17,8 +17,9 @@ const ( type Editor struct { col, row int - x, y int vrow int + w, h int + x, y int lines []string ins *Insert mode Mode @@ -26,15 +27,8 @@ type Editor struct { bell bool } -func (ed *Editor) Load() { - if ed.path == "" { - return - } - _, err := os.Stat(ed.path) - if err != nil { // file not exists - return - } - data, err := os.ReadFile(ed.path) +func (ed *Editor) Load(path string) { + data, err := os.ReadFile(path) if err != nil { panic(err) } @@ -46,6 +40,7 @@ func (ed *Editor) Load() { data = data[:len(data)-1] } ed.lines = strings.Split(string(data), "\n") + ed.path = path } func Init(args []string) *Editor { @@ -54,12 +49,15 @@ func Init(args []string) *Editor { path = args[1] } + w, h := termi.Size() ed := &Editor{ col: 0, row: 0, + vrow: 0, + w: w, + h: h, x: 0, y: 0, - vrow: 0, lines: make([]string, 1), ins: NewInsert(), mode: ModeCommand, @@ -67,21 +65,24 @@ func Init(args []string) *Editor { bell: false, } - ed.Load() + if path != "" { + _, err := os.Stat(path) + if err == nil { // file exists + ed.Load(path) + } + } termi.Raw() return ed } -func (ed *Editor) Save() { - if ed.path == "" { - return - } +func (ed *Editor) Save(path string) { text := strings.Join(ed.lines, "\n") + "\n" - err := os.WriteFile(ed.path, []byte(text), 0644) + err := os.WriteFile(path, []byte(text), 0644) if err != nil { panic(err) } + ed.path = path } func (ed *Editor) Finish() { @@ -90,16 +91,53 @@ func (ed *Editor) Finish() { termi.Cooked() termi.ShowCursor() - ed.Save() + if ed.path != "" { + ed.Save(ed.path) + } +} + +func (ed *Editor) Line(row int) string { + if ed.mode == ModeInsert && row == ed.row { + return ed.ins.Line() + } else { + return ed.lines[row] + } +} + +func (ed *Editor) CurrentLine() string { + return ed.Line(ed.row) } func (ed *Editor) RuneCount() int { - return utf8.RuneCountInString(ed.lines[ed.row]) + return utf8.RuneCountInString(ed.CurrentLine()) +} + +func (ed *Editor) Confine() { + if ed.mode != ModeCommand { + panic("invalid state") + } + + n := len(ed.lines) + if ed.row < 0 { + ed.row = 0 + } else if ed.row >= n { + ed.row = max(n-1, 0) + } + + rc := ed.RuneCount() + if ed.col < 0 { + ed.col = 0 + } else if ed.col >= rc { + ed.col = max(rc-1, 0) + } } func (ed *Editor) InsertRune(r rune) { - ed.ins.Write(r) - ed.col++ + if ed.mode != ModeInsert { + panic("invalid state") + } + ed.ins.WriteRune(r) + ed.col = ed.ins.Column() } func (ed *Editor) Ring() { diff --git a/internal/editor/insert.go b/internal/editor/insert.go index 82826f0..eb160fa 100644 --- a/internal/editor/insert.go +++ b/internal/editor/insert.go @@ -2,8 +2,6 @@ package editor import ( "unicode/utf8" - - "tea.kareha.org/lab/termi" ) type Insert struct { @@ -31,11 +29,8 @@ func (ins *Insert) Reset() { } } -func (ins *Insert) Write(r rune) { - ins.body.WriteRune(r) -} - -func (ins *Insert) Enter(line string, col int) { +func (ins *Insert) Init(line string, col int) { + ins.Reset() rs := []rune(line) ins.head = string(rs[:col]) if col < len(rs) { @@ -45,6 +40,10 @@ func (ins *Insert) Enter(line string, col int) { } } +func (ins *Insert) WriteRune(r rune) { + ins.body.WriteRune(r) +} + func (ins *Insert) Line() string { return ins.head + ins.body.String() + ins.tail } @@ -60,10 +59,9 @@ func (ins *Insert) Newline() []string { return lines } -func (ins *Insert) Width() int { +func (ins *Insert) Column() int { s := ins.head + ins.body.String() - rc := utf8.RuneCountInString(s) - return termi.StringWidth(s, rc) + return utf8.RuneCountInString(s) } func (ins *Insert) Backspace() bool { diff --git a/internal/editor/main.go b/internal/editor/main.go index e88d80b..485088e 100644 --- a/internal/editor/main.go +++ b/internal/editor/main.go @@ -5,6 +5,9 @@ import ( ) func (ed *Editor) ExitInsert() { + if ed.mode != ModeInsert { + panic("invalid state") + } ed.lines[ed.row] = ed.ins.Line() ed.ins.Reset() ed.mode = ModeCommand @@ -12,6 +15,9 @@ func (ed *Editor) ExitInsert() { } func (ed *Editor) InsertNewline() { + if ed.mode != ModeInsert { + panic("invalid state") + } before := make([]string, 0, len(ed.lines)+1) before = append(before, ed.lines[:ed.row]...) var after []string @@ -24,19 +30,24 @@ func (ed *Editor) InsertNewline() { ed.lines = append(append(before, lines...), after...) ed.row++ ed.col = 0 + // row and col are confined automatically } -func (ed *Editor) DeleteBefore() { +func (ed *Editor) Backspace() { + if ed.mode != ModeInsert { + panic("invalid state") + } if !ed.ins.Backspace() { ed.Ring() return } ed.col-- + // col is confined automatically } func (ed *Editor) Main() { for { - ed.Repaint() + ed.Draw() key := termi.ReadKey() switch ed.mode { @@ -83,9 +94,9 @@ func (ed *Editor) Main() { case termi.RuneEnter: ed.InsertNewline() case termi.RuneBackspace: - ed.DeleteBefore() + ed.Backspace() case termi.RuneDelete: - ed.DeleteBefore() + ed.Backspace() default: ed.InsertRune(key.Rune) } diff --git a/internal/editor/view.go b/internal/editor/view.go index 598d7bc..e6b877c 100644 --- a/internal/editor/view.go +++ b/internal/editor/view.go @@ -7,34 +7,26 @@ import ( ) 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 + return 1 + max(width-1, 0)/ed.w } 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.ins.Line() - } else { - line = ed.lines[i] - } + line := ed.Line(i) termi.MoveCursor(0, y) termi.Draw(line) y += ed.LineHeight(line) - if y >= h-1 { + if y >= ed.h-1 { break } } - for ; y < h-1; y++ { + for ; y < ed.h-1; y++ { termi.MoveCursor(0, y) termi.Draw("~") } @@ -49,8 +41,7 @@ func (ed *Editor) DrawStatus() { m = "i" } - _, h := termi.Size() - termi.MoveCursor(0, h-1) + termi.MoveCursor(0, ed.h-1) if ed.bell { termi.EnableInvert() } @@ -62,25 +53,10 @@ func (ed *Editor) DrawStatus() { } func (ed *Editor) UpdateCursor() { - w, h := termi.Size() - - var dy int - switch ed.mode { - case ModeCommand: - 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)) - - // XXX approximation - width := termi.StringWidth(ed.lines[ed.row], ed.col) - ed.x = width % w - dy = width / w - case ModeInsert: - // XXX approximation - width := ed.ins.Width() - ed.x = width % w - dy = width / w - } + // XXX approximation + width := termi.StringWidth(ed.CurrentLine(), ed.col) + ed.x = width % ed.w + dy := width / ed.w if ed.row < ed.vrow { ed.vrow = ed.row @@ -92,7 +68,7 @@ func (ed *Editor) UpdateCursor() { } ed.y = y + dy - for ed.y >= h-1 { + for ed.y >= ed.h-1 { ed.vrow++ y := 0 @@ -104,6 +80,10 @@ func (ed *Editor) UpdateCursor() { } func (ed *Editor) Repaint() { + w, h := termi.Size() + ed.w = w + ed.h = h + termi.HideCursor() termi.Clear() @@ -118,3 +98,7 @@ func (ed *Editor) Repaint() { termi.ShowCursor() } + +func (ed *Editor) Draw() { + ed.Repaint() +}