Files
2026-10-26 02:32:03 +00:00

219 lines
4.3 KiB
Go

package buffer
import (
"log/slog"
"unicode/utf8"
"code.chimeric.al/dwrz/wen/internal/editor/buffer/glyph"
)
// Cursor tracks the position in a Buffer.
// glyph is the 0-indexed terminal display column (tabs are 8 columns, wide
// runes are 2 columns).
// index is the 0-indexed byte offset in the current line's UTF-8 string.
// line is the 0-indexed row in Buffer.lines.
type Cursor struct {
glyph int
index int
line int
}
func (b *Buffer) Cursor() Cursor {
return *b.cursor
}
func (c Cursor) Glyph() int {
return c.glyph
}
func (c Cursor) Index() int {
return c.index
}
func (c Cursor) Line() int {
return c.line
}
func (b *Buffer) CursorDown() {
b.log.Debug("↓ before", slog.Any("cursor", b.cursor))
defer b.log.Debug("↓ after", slog.Any("cursor", b.cursor))
// If we're on the last line, don't move down.
if b.cursor.line >= len(b.lines)-1 {
return
}
// Move to the next line.
b.cursor.line++
// Adjust the column.
var (
line = b.CursorLine()
length = line.Length()
width = line.Width()
)
switch {
// If we've moved to a shorter line, snap to its end.
case b.cursor.glyph > width:
b.cursor.index = length
b.cursor.glyph = width
default:
b.cursor.index, b.cursor.glyph = line.FindGlyphIndex(
b.cursor.glyph,
)
}
}
func (b *Buffer) CursorLeft() {
b.log.Debug("← before", slog.Any("cursor", b.cursor))
defer b.log.Debug("← after", slog.Any("cursor", b.cursor))
switch {
// If we're at the beginning of the line, move to the line above;
// unless we're at the start of the buffer.
case b.cursor.index == 0 && b.cursor.line > 0:
b.cursor.line--
line := b.CursorLine()
b.cursor.index = line.Length()
b.cursor.glyph = line.Width()
// Move left by one rune.
case b.cursor.index > 0:
var (
line = b.CursorLine()
r rune
size int
)
// Reverse until we hit the start of a rune.
for i := b.cursor.index - 1; i >= 0; i-- {
r, size = line.DecodeRune(i)
if r != utf8.RuneError {
b.cursor.index -= size
b.cursor.glyph -= glyph.Width(r)
break
}
}
}
}
func (b *Buffer) CursorRight() {
b.log.Debug("→ before", slog.Any("cursor", b.cursor))
defer b.log.Debug("→ after", slog.Any("cursor", b.cursor))
var (
line = b.CursorLine()
length = line.Length()
)
switch {
// If we're at the end of the line, move to the line below;
// unless we're at the end of the buffer.
case b.cursor.index == length && b.cursor.line < len(b.lines)-1:
b.cursor.line++
b.cursor.index = 0
b.cursor.glyph = 0
// Move the index right by one rune.
case b.cursor.index < length:
r, size := line.DecodeRune(b.cursor.index)
if r == utf8.RuneError {
b.cursor.index++
b.cursor.glyph += glyph.Width(r)
} else {
b.cursor.index += size
b.cursor.glyph += glyph.Width(r)
}
}
}
func (b *Buffer) CursorUp() {
b.log.Debug("↑ before", slog.Any("cursor", b.cursor))
defer b.log.Debug("↑ after", slog.Any("cursor", b.cursor))
// If we're on the first line, don't move up.
if b.cursor.line == 0 {
return
}
b.cursor.line--
// Adjust the column.
var (
line = b.CursorLine()
length = line.Length()
width = line.Width()
)
switch {
case b.cursor.glyph > width:
b.cursor.index = length
b.cursor.glyph = width
default:
b.cursor.index, b.cursor.glyph = line.FindGlyphIndex(
b.cursor.glyph,
)
}
}
func (b *Buffer) PageDown(height int) {
if b.cursor.line+height >= len(b.lines) {
b.cursor.line = len(b.lines) - 1
} else {
b.cursor.line += height
}
var (
line = b.CursorLine()
length = line.Length()
width = line.Width()
)
switch {
case b.cursor.glyph > width:
b.cursor.index = length
b.cursor.glyph = width
default:
b.cursor.index, b.cursor.glyph = line.FindGlyphIndex(
b.cursor.glyph,
)
}
}
func (b *Buffer) PageUp(height int) {
if b.cursor.line-height > 0 {
b.cursor.line -= height
} else {
b.cursor.line = 0
}
var (
line = b.CursorLine()
length = line.Length()
width = line.Width()
)
switch {
case b.cursor.glyph > width:
b.cursor.index = length
b.cursor.glyph = width
default:
b.cursor.index, b.cursor.glyph = line.FindGlyphIndex(
b.cursor.glyph,
)
}
}
func (b *Buffer) CursorHome() {
b.cursor.glyph = 0
b.cursor.index = 0
}
func (b *Buffer) CursorEnd() {
var line = b.CursorLine()
b.cursor.index = line.Length()
b.cursor.glyph = line.Width()
}