This commit is contained in:
2026-03-27 12:08:31 +09:00
parent b6066ed3ec
commit 0b51c76132
5 changed files with 112 additions and 80 deletions

View File

@@ -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()
}

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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()
}