219 lines
4.3 KiB
Go
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()
|
||
|
|
}
|