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 }