This commit is contained in:
2026-03-26 12:21:48 +09:00
parent 24cdd59e12
commit 691f8d4175
5 changed files with 280 additions and 251 deletions

View File

@@ -9,7 +9,7 @@ import (
type Key int
const (
KeyNormal = iota
KeyNormal Key = iota
KeyUp
KeyDown
KeyRight

View File

@@ -8,9 +8,9 @@ func (ed *Editor) enterInsert() {
}
func (ed *Editor) enterInsertAfter() {
len := ed.runeCount()
if ed.col >= len-1 {
ed.col = len
rc := ed.runeCount()
if ed.col >= rc-1 {
ed.col = rc
ed.head = ed.lines[ed.row]
ed.tail = ""
ed.mode = modeInsert

View File

@@ -12,7 +12,7 @@ import (
type mode int
const (
modeCommand = iota
modeCommand mode = iota
modeInsert
)
@@ -27,44 +27,63 @@ type Editor struct {
path string
}
func (ed *Editor) load() {
if ed.path == "" {
return
}
_, err := os.Stat(ed.path)
if err != nil { // file not exists
return
}
data, err := ioutil.ReadFile(ed.path)
if err != nil {
panic(err)
}
if len(data) < 1 {
ed.lines = make([]string, 1)
}
// TODO CRLF
if data[len(data)-1] == '\n' {
data = data[:len(data)-1]
}
ed.lines = strings.Split(string(data), "\n")
}
func Init(args []string) *Editor {
var path string
var lines []string
if len(args) > 1 {
path = args[1]
_, err := os.Stat(path)
if err == nil { // file exists
data, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
if len(data) > 0 {
if data[len(data)-1] == '\n' {
data = data[:len(data)-1]
}
lines = strings.Split(string(data), "\n")
}
}
}
if len(lines) < 1 {
lines = make([]string, 1)
}
console.Raw()
return &Editor{
ed := &Editor{
col: 0,
row: 0,
x: 0,
y: 0,
vrow: 0,
lines: lines,
lines: make([]string, 1),
head: "",
tail: "",
insert: new(strings.Builder),
mode: modeCommand,
path: path,
}
ed.load()
console.Raw()
return ed
}
func (ed *Editor) save() {
if ed.path == "" {
return
}
text := strings.Join(ed.lines, "\n") + "\n"
err := ioutil.WriteFile(ed.path, []byte(text), 0644)
if err != nil {
panic(err)
}
}
func (ed *Editor) Finish() {
@@ -73,238 +92,14 @@ func (ed *Editor) Finish() {
console.Cooked()
console.ShowCursor()
if ed.path != "" {
text := strings.Join(ed.lines, "\n") + "\n"
err := ioutil.WriteFile(ed.path, []byte(text), 0644)
if err != nil {
panic(err)
}
}
ed.save()
}
func (ed *Editor) runeCount() int {
return utf8.RuneCountInString(ed.lines[ed.row])
}
func (ed *Editor) lineHeight(line string) int {
w, _ := console.Size()
rc := utf8.RuneCountInString(line)
width := console.StringWidth(line, rc)
return 1 + max(width-1, 0)/w
}
func (ed *Editor) drawBuffer() {
_, h := console.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
} else {
line = ed.lines[i]
}
console.MoveCursor(0, y)
console.Print(line)
y += ed.lineHeight(line)
if y >= h-1 {
break
}
}
for ; y < h-1; y++ {
console.MoveCursor(0, y)
console.Print("~")
}
}
func (ed *Editor) drawStatus() {
_, h := console.Size()
console.MoveCursor(0, h-1)
switch ed.mode {
case modeCommand:
console.Print("c")
case modeInsert:
console.Print("i")
}
}
func (ed *Editor) updateCursor() {
w, h := console.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 := console.StringWidth(ed.lines[ed.row], ed.col)
ed.x = width % w
dy = width / w
case modeInsert:
// XXX approximation
width := console.StringWidth(ed.head+ed.insert.String(), ed.col)
ed.x = width % w
dy = width / w
}
if ed.row < ed.vrow {
ed.vrow = ed.row
}
y := 0
for i := ed.vrow; i < ed.row; i++ {
y += ed.lineHeight(ed.lines[i])
}
ed.y = y + dy
for ed.y >= h-1 {
ed.vrow++
y := 0
for i := ed.vrow; i < ed.row; i++ {
y += ed.lineHeight(ed.lines[i])
}
ed.y = y + dy
}
}
func (ed *Editor) repaint() {
console.HideCursor()
console.Clear()
console.HomeCursor()
ed.updateCursor()
ed.drawBuffer()
ed.drawStatus()
console.MoveCursor(ed.x, ed.y)
console.ShowCursor()
}
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) insertNewline() {
before := make([]string, 0, len(ed.lines)+1)
before = append(before, ed.lines[:ed.row]...)
var after []string
if ed.row+1 < len(ed.lines) {
after = ed.lines[ed.row+1:]
} else {
after = []string{}
}
newLines := []string{
ed.head + ed.insert.String(),
ed.tail,
}
ed.lines = append(append(before, newLines...), after...)
ed.row++
ed.col = 0
ed.head = ""
ed.insert.Reset()
}
func (ed *Editor) deleteBefore() {
if ed.insert.Len() < 1 {
return // TODO ring
}
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) insertRune(r rune) {
ed.insert.WriteRune(r)
ed.col++
}
func (ed *Editor) Main() {
for {
ed.repaint()
k, r := console.ReadKey()
switch ed.mode {
case modeCommand:
switch k {
case console.KeyNormal:
switch r {
case 'q':
return
case 'i':
ed.enterInsert()
case 'a':
ed.enterInsertAfter()
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)
}
case console.KeyUp:
ed.moveUp(1)
case console.KeyDown:
ed.moveDown(1)
case console.KeyRight:
ed.moveRight(1)
case console.KeyLeft:
ed.moveLeft(1)
default:
// TODO ring
}
case modeInsert:
switch k {
case console.KeyNormal:
switch r {
case console.RuneEscape:
ed.exitInsert()
case console.RuneEnter:
ed.insertNewline()
case console.RuneBackspace:
ed.deleteBefore()
case console.RuneDelete:
ed.deleteBefore()
default:
ed.insertRune(r)
}
case console.KeyUp:
ed.exitInsert()
ed.moveUp(1)
case console.KeyDown:
ed.exitInsert()
ed.moveDown(1)
case console.KeyRight:
ed.exitInsert()
ed.moveRight(1)
case console.KeyLeft:
ed.exitInsert()
ed.moveLeft(1)
default:
// TODO ring
}
}
}
}

121
internal/editor/main.go Normal file
View File

@@ -0,0 +1,121 @@
package editor
import (
"unicode/utf8"
"tea.kareha.org/lab/levi/internal/console"
)
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) insertNewline() {
before := make([]string, 0, len(ed.lines)+1)
before = append(before, ed.lines[:ed.row]...)
var after []string
if ed.row+1 < len(ed.lines) {
after = ed.lines[ed.row+1:]
} else {
after = []string{}
}
newLines := []string{
ed.head + ed.insert.String(),
ed.tail,
}
ed.lines = append(append(before, newLines...), after...)
ed.row++
ed.col = 0
ed.head = ""
ed.insert.Reset()
}
func (ed *Editor) deleteBefore() {
if ed.insert.Len() < 1 {
return // TODO ring
}
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()
k, r := console.ReadKey()
switch ed.mode {
case modeCommand:
switch k {
case console.KeyNormal:
switch r {
case 'q':
return
case 'i':
ed.enterInsert()
case 'a':
ed.enterInsertAfter()
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)
}
case console.KeyUp:
ed.moveUp(1)
case console.KeyDown:
ed.moveDown(1)
case console.KeyRight:
ed.moveRight(1)
case console.KeyLeft:
ed.moveLeft(1)
default:
// TODO ring
}
case modeInsert:
switch k {
case console.KeyNormal:
switch r {
case console.RuneEscape:
ed.exitInsert()
case console.RuneEnter:
ed.insertNewline()
case console.RuneBackspace:
ed.deleteBefore()
case console.RuneDelete:
ed.deleteBefore()
default:
ed.insertRune(r)
}
case console.KeyUp:
ed.exitInsert()
ed.moveUp(1)
case console.KeyDown:
ed.exitInsert()
ed.moveDown(1)
case console.KeyRight:
ed.exitInsert()
ed.moveRight(1)
case console.KeyLeft:
ed.exitInsert()
ed.moveLeft(1)
default:
// TODO ring
}
}
}
}

113
internal/editor/view.go Normal file
View File

@@ -0,0 +1,113 @@
package editor
import (
"unicode/utf8"
"tea.kareha.org/lab/levi/internal/console"
)
func (ed *Editor) lineHeight(line string) int {
w, _ := console.Size()
rc := utf8.RuneCountInString(line)
width := console.StringWidth(line, rc)
return 1 + max(width-1, 0)/w
}
func (ed *Editor) drawBuffer() {
_, h := console.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
} else {
line = ed.lines[i]
}
console.MoveCursor(0, y)
console.Print(line)
y += ed.lineHeight(line)
if y >= h-1 {
break
}
}
for ; y < h-1; y++ {
console.MoveCursor(0, y)
console.Print("~")
}
}
func (ed *Editor) drawStatus() {
var m string
switch ed.mode {
case modeCommand:
m = "c"
case modeInsert:
m = "i"
}
_, h := console.Size()
console.MoveCursor(0, h-1)
console.Printf("%s %d,%d %s", m, ed.row, ed.col, ed.path)
}
func (ed *Editor) updateCursor() {
w, h := console.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 := console.StringWidth(ed.lines[ed.row], ed.col)
ed.x = width % w
dy = width / w
case modeInsert:
// XXX approximation
width := console.StringWidth(ed.head+ed.insert.String(), ed.col)
ed.x = width % w
dy = width / w
}
if ed.row < ed.vrow {
ed.vrow = ed.row
}
y := 0
for i := ed.vrow; i < ed.row; i++ {
y += ed.lineHeight(ed.lines[i])
}
ed.y = y + dy
for ed.y >= h-1 {
ed.vrow++
y := 0
for i := ed.vrow; i < ed.row; i++ {
y += ed.lineHeight(ed.lines[i])
}
ed.y = y + dy
}
}
func (ed *Editor) repaint() {
console.HideCursor()
console.Clear()
console.HomeCursor()
ed.updateCursor()
ed.drawBuffer()
ed.drawStatus()
console.MoveCursor(ed.x, ed.y)
console.ShowCursor()
}