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