This is so vibe coded, but in the interest of time, its a bit necessary. Plus this is a complex problem that I don't have the mental bandwidth to invest right now.
426 lines
10 KiB
Go
426 lines
10 KiB
Go
package core
|
|
|
|
import "strings"
|
|
|
|
type BufferOptions struct {
|
|
// tabstop expandtab
|
|
}
|
|
|
|
type BufferChangeKind int
|
|
|
|
const (
|
|
BufferChangeSetLine BufferChangeKind = iota
|
|
BufferChangeInsertLine
|
|
BufferChangeDeleteLine
|
|
BufferChangeSetLines
|
|
)
|
|
|
|
type BufferChange struct {
|
|
Kind BufferChangeKind
|
|
StartLine int
|
|
EndLine int
|
|
Edit *BufferEdit
|
|
}
|
|
|
|
// TextPoint is a byte-oriented row/column point.
|
|
//
|
|
// Column is counted in bytes (Tree-sitter compatible), not runes.
|
|
type TextPoint struct {
|
|
Row uint
|
|
Column uint
|
|
}
|
|
|
|
// BufferEdit represents a text edit in byte offsets and points.
|
|
//
|
|
// These fields map directly to Tree-sitter incremental edit inputs.
|
|
type BufferEdit struct {
|
|
StartByte uint
|
|
OldEndByte uint
|
|
NewEndByte uint
|
|
|
|
StartPoint TextPoint
|
|
OldEndPoint TextPoint
|
|
NewEndPoint TextPoint
|
|
}
|
|
|
|
type BufferType int
|
|
|
|
const (
|
|
ScatchBuffer BufferType = iota
|
|
FileBuffer
|
|
DirectoryBuffer
|
|
)
|
|
|
|
type Buffer struct {
|
|
// Buffer data
|
|
Id int
|
|
Type BufferType
|
|
|
|
// File data
|
|
Filename string
|
|
Filetype string
|
|
Lines []*GapBuffer // Changed from []string to []*GapBuffer
|
|
|
|
// Flags (not used yet)
|
|
Modified bool
|
|
Loaded bool
|
|
Listed bool
|
|
ReadOnly bool
|
|
|
|
// Options BufferOptions
|
|
UndoStack *UndoStack
|
|
|
|
// Optional change callback used by higher layers (editor/syntax) to react to edits.
|
|
OnChange func(change BufferChange)
|
|
}
|
|
|
|
// ==================================================
|
|
// Helper methods
|
|
// ==================================================
|
|
|
|
// Buffer.Line: Get the line at an index. Returns an empty string if the index
|
|
// is out of bounds.
|
|
func (b *Buffer) Line(idx int) string {
|
|
if idx < 0 || idx >= len(b.Lines) {
|
|
return ""
|
|
}
|
|
return b.Lines[idx].String()
|
|
}
|
|
|
|
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
|
|
// index is out of bounds. This function sets the modified flag.
|
|
func (b *Buffer) SetLine(idx int, content string) {
|
|
oldSource := b.sourceString()
|
|
changed := false
|
|
if idx >= 0 && idx < len(b.Lines) {
|
|
// Record set line in undo stack
|
|
if b.UndoStack != nil {
|
|
b.UndoStack.RecordSetLine(idx, b.Lines[idx].String(), content)
|
|
}
|
|
b.Lines[idx].Set(content)
|
|
changed = true
|
|
}
|
|
b.Modified = true
|
|
if changed {
|
|
newSource := b.sourceString()
|
|
edit, ok := computeBufferEdit(oldSource, newSource)
|
|
change := BufferChange{Kind: BufferChangeSetLine, StartLine: idx, EndLine: idx}
|
|
if ok {
|
|
change.Edit = &edit
|
|
}
|
|
b.notifyChange(change)
|
|
}
|
|
}
|
|
|
|
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
|
|
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
|
|
// the given index. This function sets the modified flag.
|
|
func (b *Buffer) InsertLine(idx int, content string) {
|
|
oldSource := b.sourceString()
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
if idx > len(b.Lines) {
|
|
idx = len(b.Lines)
|
|
}
|
|
|
|
// Record insert line in undo stack
|
|
if b.UndoStack != nil {
|
|
b.UndoStack.RecordInsertLine(idx, content)
|
|
}
|
|
|
|
newLine := NewGapBuffer(content)
|
|
b.Lines = append(b.Lines[:idx], append([]*GapBuffer{newLine}, b.Lines[idx:]...)...)
|
|
b.Modified = true
|
|
|
|
newSource := b.sourceString()
|
|
edit, ok := computeBufferEdit(oldSource, newSource)
|
|
change := BufferChange{Kind: BufferChangeInsertLine, StartLine: idx, EndLine: len(b.Lines) - 1}
|
|
if ok {
|
|
change.Edit = &edit
|
|
}
|
|
b.notifyChange(change)
|
|
}
|
|
|
|
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
|
|
// of bounds. This function sets the modified flag.
|
|
func (b *Buffer) DeleteLine(idx int) {
|
|
oldSource := b.sourceString()
|
|
changed := false
|
|
if idx >= 0 && idx < len(b.Lines) {
|
|
// Record delete line in undo stack
|
|
if b.UndoStack != nil {
|
|
b.UndoStack.RecordDeleteLine(idx, b.Lines[idx].String())
|
|
}
|
|
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
|
|
changed = true
|
|
}
|
|
b.Modified = true
|
|
if changed {
|
|
newSource := b.sourceString()
|
|
edit, ok := computeBufferEdit(oldSource, newSource)
|
|
change := BufferChange{Kind: BufferChangeDeleteLine, StartLine: idx, EndLine: len(b.Lines) - 1}
|
|
if ok {
|
|
change.Edit = &edit
|
|
}
|
|
b.notifyChange(change)
|
|
}
|
|
}
|
|
|
|
// Buffer.LineCount: Get the number of lines in the buffer.
|
|
func (b *Buffer) LineCount() int {
|
|
return len(b.Lines)
|
|
}
|
|
|
|
// ==================================================
|
|
// Undo Stack
|
|
// ==================================================
|
|
func (b *Buffer) Undo(w *Window) bool {
|
|
if b.UndoStack == nil {
|
|
return false
|
|
}
|
|
|
|
oldSource := b.sourceString()
|
|
|
|
block := b.UndoStack.Undo()
|
|
if block == nil {
|
|
return false
|
|
}
|
|
|
|
// Apply changes in REVERSE order
|
|
for i := len(block.Changes) - 1; i >= 0; i-- {
|
|
change := block.Changes[i]
|
|
|
|
// Temporarily disable recording while we undo
|
|
wasRecording := b.UndoStack.recording
|
|
b.UndoStack.recording = false
|
|
|
|
switch change.Type {
|
|
case SetLineChange:
|
|
// Restore old data
|
|
if change.Line >= 0 && change.Line < len(b.Lines) {
|
|
b.Lines[change.Line].Set(change.OldData)
|
|
}
|
|
case InsertLineChange:
|
|
// Remove the inserted line
|
|
if change.Line >= 0 && change.Line < len(b.Lines) {
|
|
b.Lines = append(b.Lines[:change.Line], b.Lines[change.Line+1:]...)
|
|
}
|
|
case DeleteLineChange:
|
|
// Re-insert the deleted line
|
|
if change.Line <= len(b.Lines) {
|
|
newLine := NewGapBuffer(change.OldData)
|
|
b.Lines = append(b.Lines[:change.Line], append([]*GapBuffer{newLine}, b.Lines[change.Line:]...)...)
|
|
}
|
|
}
|
|
|
|
b.UndoStack.recording = wasRecording
|
|
}
|
|
|
|
// Restore cursor position
|
|
w.SetCursorLine(block.OldCursor.Line)
|
|
w.SetCursorCol(block.OldCursor.Col)
|
|
|
|
newSource := b.sourceString()
|
|
if edit, ok := computeBufferEdit(oldSource, newSource); ok {
|
|
b.notifyChange(BufferChange{
|
|
Kind: BufferChangeSetLines,
|
|
StartLine: 0,
|
|
EndLine: max(0, len(b.Lines)-1),
|
|
Edit: &edit,
|
|
})
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (b *Buffer) Redo(w *Window) bool {
|
|
if b.UndoStack == nil {
|
|
return false
|
|
}
|
|
|
|
oldSource := b.sourceString()
|
|
|
|
block := b.UndoStack.Redo()
|
|
if block == nil {
|
|
return false
|
|
}
|
|
|
|
// Apply changes in FORWARD order
|
|
for _, change := range block.Changes {
|
|
// Temporarily disable recording while we redo
|
|
wasRecording := b.UndoStack.recording
|
|
b.UndoStack.recording = false
|
|
|
|
switch change.Type {
|
|
case SetLineChange:
|
|
// Apply new data
|
|
if change.Line >= 0 && change.Line < len(b.Lines) {
|
|
b.Lines[change.Line].Set(change.NewData)
|
|
}
|
|
case InsertLineChange:
|
|
// Re-insert the line
|
|
if change.Line <= len(b.Lines) {
|
|
newLine := NewGapBuffer(change.NewData)
|
|
b.Lines = append(b.Lines[:change.Line], append([]*GapBuffer{newLine}, b.Lines[change.Line:]...)...)
|
|
}
|
|
case DeleteLineChange:
|
|
// Re-delete the line
|
|
if change.Line >= 0 && change.Line < len(b.Lines) {
|
|
b.Lines = append(b.Lines[:change.Line], b.Lines[change.Line+1:]...)
|
|
}
|
|
}
|
|
|
|
b.UndoStack.recording = wasRecording
|
|
}
|
|
|
|
// Restore cursor position
|
|
w.SetCursorLine(block.NewCursor.Line)
|
|
w.SetCursorCol(block.NewCursor.Col)
|
|
|
|
newSource := b.sourceString()
|
|
if edit, ok := computeBufferEdit(oldSource, newSource); ok {
|
|
b.notifyChange(BufferChange{
|
|
Kind: BufferChangeSetLines,
|
|
StartLine: 0,
|
|
EndLine: max(0, len(b.Lines)-1),
|
|
Edit: &edit,
|
|
})
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ==================================================
|
|
// Setters
|
|
// ==================================================
|
|
|
|
// Buffer.SetFilename: Set the filename associated with this buffer. This is
|
|
// typically the path to the file on disk that this buffer represents.
|
|
func (b *Buffer) SetFilename(filename string) {
|
|
b.Filename = filename
|
|
}
|
|
|
|
// Buffer.SetFiletype: Set the filetype of this buffer. The filetype is used
|
|
// for syntax highlighting and other language-specific features.
|
|
func (b *Buffer) SetFiletype(filetype string) {
|
|
b.Filetype = filetype
|
|
}
|
|
|
|
// Buffer.SetLines: Replace all lines in the buffer with the provided lines.
|
|
// This is useful when loading a file or resetting buffer content.
|
|
func (b *Buffer) SetLines(lines []string) {
|
|
oldSource := b.sourceString()
|
|
b.Lines = make([]*GapBuffer, len(lines))
|
|
for i, line := range lines {
|
|
b.Lines[i] = NewGapBuffer(line)
|
|
}
|
|
|
|
newSource := b.sourceString()
|
|
edit, ok := computeBufferEdit(oldSource, newSource)
|
|
change := BufferChange{Kind: BufferChangeSetLines, StartLine: 0, EndLine: len(lines) - 1}
|
|
if ok {
|
|
change.Edit = &edit
|
|
}
|
|
b.notifyChange(change)
|
|
}
|
|
|
|
func (b *Buffer) notifyChange(change BufferChange) {
|
|
if b.OnChange != nil {
|
|
b.OnChange(change)
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) sourceString() string {
|
|
if len(b.Lines) == 0 {
|
|
return ""
|
|
}
|
|
|
|
parts := make([]string, len(b.Lines))
|
|
for i := range b.Lines {
|
|
parts[i] = b.Lines[i].String()
|
|
}
|
|
|
|
return strings.Join(parts, "\n")
|
|
}
|
|
|
|
func computeBufferEdit(oldSource, newSource string) (BufferEdit, bool) {
|
|
if oldSource == newSource {
|
|
return BufferEdit{}, false
|
|
}
|
|
|
|
oldBytes := []byte(oldSource)
|
|
newBytes := []byte(newSource)
|
|
|
|
prefix := 0
|
|
maxPrefix := min(len(oldBytes), len(newBytes))
|
|
for prefix < maxPrefix && oldBytes[prefix] == newBytes[prefix] {
|
|
prefix++
|
|
}
|
|
|
|
oldEnd := len(oldBytes)
|
|
newEnd := len(newBytes)
|
|
for oldEnd > prefix && newEnd > prefix && oldBytes[oldEnd-1] == newBytes[newEnd-1] {
|
|
oldEnd--
|
|
newEnd--
|
|
}
|
|
|
|
edit := BufferEdit{
|
|
StartByte: uint(prefix),
|
|
OldEndByte: uint(oldEnd),
|
|
NewEndByte: uint(newEnd),
|
|
StartPoint: byteOffsetToPoint(oldBytes, prefix),
|
|
OldEndPoint: byteOffsetToPoint(oldBytes, oldEnd),
|
|
NewEndPoint: byteOffsetToPoint(newBytes, newEnd),
|
|
}
|
|
|
|
return edit, true
|
|
}
|
|
|
|
func byteOffsetToPoint(src []byte, offset int) TextPoint {
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset > len(src) {
|
|
offset = len(src)
|
|
}
|
|
|
|
var row uint
|
|
var col uint
|
|
for i := 0; i < offset; i++ {
|
|
if src[i] == '\n' {
|
|
row++
|
|
col = 0
|
|
} else {
|
|
col++
|
|
}
|
|
}
|
|
|
|
return TextPoint{Row: row, Column: col}
|
|
}
|
|
|
|
// Buffer.SetModified: Set the modified flag for this buffer. A modified buffer
|
|
// has unsaved changes that differ from the file on disk.
|
|
func (b *Buffer) SetModified(modified bool) {
|
|
b.Modified = modified
|
|
}
|
|
|
|
// Buffer.SetLoaded: Set the loaded flag for this buffer. A loaded buffer has
|
|
// its content in memory, while an unloaded buffer exists only as metadata.
|
|
func (b *Buffer) SetLoaded(loaded bool) {
|
|
b.Loaded = loaded
|
|
}
|
|
|
|
// Buffer.SetListed: Set the listed flag for this buffer. A listed buffer appears
|
|
// in buffer lists (like :ls), while an unlisted buffer is hidden from normal
|
|
// buffer navigation.
|
|
func (b *Buffer) SetListed(listed bool) {
|
|
b.Listed = listed
|
|
}
|
|
|
|
// Buffer.SetType: Set the buffers type. This type is used to determine handling
|
|
// of I/O functions.
|
|
func (b *Buffer) SetType(t BufferType) {
|
|
b.Type = t
|
|
}
|