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 }