Gim/internal/core/undo.go
Hayden Hargreaves 98e02553b1 feat: working on the undo stack! Huge progress, not tested
Tests are coming, but there are some infrastructure issues with the
tests
2026-03-30 22:38:33 -07:00

181 lines
3.2 KiB
Go

package core
import (
"fmt"
"slices"
)
type ChangeType string
const (
SetLineChange ChangeType = "SetLine"
InsertLineChange ChangeType = "InsertLine"
DeleteLineChange ChangeType = "DeleteLine"
)
type Change struct {
Type ChangeType
Line int
OldData string
NewData string
}
type ChangeBlock struct {
Changes []Change
OldCursor Position // Before OP
NewCursor Position // After OP
}
type UndoStack struct {
undoStack []ChangeBlock
redoStack []ChangeBlock
current []Change
recording bool
oldCursor Position
}
func NewUndoStack() *UndoStack {
return &UndoStack{
undoStack: []ChangeBlock{},
redoStack: []ChangeBlock{},
current: []Change{},
recording: false,
oldCursor: Position{},
}
}
func (u *UndoStack) BeginBlock(cursor Position) {
u.current = []Change{}
u.recording = true
u.oldCursor = cursor
}
func (u *UndoStack) EndBlock(cursor Position) {
// If not recording or nothing changed, we can exit safely
if !u.recording || len(u.current) == 0 {
return
}
block := ChangeBlock{
Changes: u.current,
OldCursor: u.oldCursor,
NewCursor: cursor,
}
u.undoStack = append(u.undoStack, block)
u.redoStack = []ChangeBlock{} // Reset old changes, can no longer redo
u.recording = false
u.current = []Change{}
}
func (u *UndoStack) RecordSetLine(line int, oldData, newData string) {
if !u.recording {
return
}
change := Change{
Type: SetLineChange,
Line: line,
OldData: oldData,
NewData: newData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) RecordInsertLine(line int, newData string) {
if !u.recording {
return
}
change := Change{
Type: InsertLineChange,
Line: line,
NewData: newData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) RecordDeleteLine(line int, oldData string) {
if !u.recording {
return
}
change := Change{
Type: DeleteLineChange,
Line: line,
OldData: oldData,
}
u.current = append(u.current, change)
}
func (u *UndoStack) Undo() *ChangeBlock {
if len(u.undoStack) == 0 {
return nil
}
// Pop from undo stack
size := len(u.undoStack)
block := u.undoStack[size-1]
u.undoStack = u.undoStack[:size-1]
// Push to redo stack
u.redoStack = append(u.redoStack, block)
return &block
}
func (u *UndoStack) Redo() *ChangeBlock {
if len(u.redoStack) == 0 {
return nil
}
// Pop from redo stack
size := len(u.redoStack)
block := u.redoStack[size-1]
u.redoStack = u.redoStack[:size-1]
// Push to undo stack
u.undoStack = append(u.undoStack, block)
return &block
}
func (u *UndoStack) CanUndo() bool {
return len(u.undoStack) > 0
}
func (u *UndoStack) CanRedo() bool {
return len(u.redoStack) > 0
}
func (u *UndoStack) Recording() bool {
return u.recording
}
func (u *UndoStack) List() []string {
var lines []string
stack := slices.Clone(u.undoStack)
slices.Reverse(stack)
for _, b := range stack {
lines = append(lines, fmt.Sprintf(
"block (%d:%d) -> (%d:%d)",
b.OldCursor.Line,
b.OldCursor.Col,
b.NewCursor.Line,
b.NewCursor.Col,
))
for _, c := range b.Changes {
lines = append(lines, fmt.Sprintf(
"\t%q #%d (%s) -> (%s)",
c.Type,
c.Line,
c.OldData,
c.NewData,
))
}
}
return lines
}