package buffer // maxUndoDepth caps the history so that a long editing session cannot grow the // process without bound. Turbo C offered a handful of undo levels; a thousand // is generous while still being a hard ceiling. const maxUndoDepth = 1000 // edit is one reversible change: the span that was replaced, the text that was // there before, the text that took its place, and where the cursor sat on // either side. // // Storing both sides makes undo and redo the same operation with the two texts // swapped, which is why there is only one kind of history entry. type edit struct { rng Range old [][]rune new [][]rune cursorBefore Position cursorAfter Position } // isInsertion reports whether the edit only added text. func (e edit) isInsertion() bool { return isEmptyText(e.old) } // isDeletion reports whether the edit only removed text. func (e edit) isDeletion() bool { return isEmptyText(e.new) } // CanUndo reports whether there is anything to undo. func (b *Buffer) CanUndo() bool { return len(b.undoStack) > 0 } // CanRedo reports whether there is anything to redo. func (b *Buffer) CanRedo() bool { return len(b.redoStack) > 0 } // Undo reverts the most recent change and reports whether it did anything. // The cursor goes back where it was before that change. // // b := buffer.NewFromString("hello") // b.MoveBufferEnd() // b.Insert(" world") // b.Undo() // fmt.Println(b.Text()) // hello func (b *Buffer) Undo() bool { if len(b.undoStack) == 0 { return false } change := b.undoStack[len(b.undoStack)-1] b.undoStack = b.undoStack[:len(b.undoStack)-1] b.splice(Range{Start: change.rng.Start, End: endOf(change.rng.Start, change.new)}, change.old) b.cursor = b.clamp(change.cursorBefore) b.redoStack = append(b.redoStack, change) b.afterHistoryMove() return true } // Redo re-applies the change most recently undone and reports whether it did // anything. Any new edit clears the redo stack, as it does in every editor. func (b *Buffer) Redo() bool { if len(b.redoStack) == 0 { return false } change := b.redoStack[len(b.redoStack)-1] b.redoStack = b.redoStack[:len(b.redoStack)-1] b.splice(change.rng, change.new) b.cursor = b.clamp(change.cursorAfter) b.undoStack = append(b.undoStack, change) b.afterHistoryMove() return true } // afterHistoryMove restores the shared state that undo and redo both disturb. func (b *Buffer) afterHistoryMove() { b.modified = true b.revision++ b.ClearSelection() b.coalesce = false } // record pushes change onto the undo history, merging it into the previous // entry when it continues a run of typing or of backspaces. func (b *Buffer) record(change edit) { b.redoStack = nil if b.coalesce && b.mergeIntoLast(change) { b.coalesce = false return } b.undoStack = append(b.undoStack, change) if len(b.undoStack) > maxUndoDepth { b.undoStack = b.undoStack[len(b.undoStack)-maxUndoDepth:] } b.coalesce = false } // mergeIntoLast folds change into the entry on top of the undo stack and // reports whether it could. Only single-line runs of typing or of backspaces // merge; anything else stays a step of its own. func (b *Buffer) mergeIntoLast(change edit) bool { if len(b.undoStack) == 0 { return false } last := &b.undoStack[len(b.undoStack)-1] switch { case last.isInsertion() && change.isInsertion() && continuesTyping(*last, change): last.new[0] = concat(last.new[0], change.new[0]) last.cursorAfter = change.cursorAfter return true case last.isDeletion() && change.isDeletion() && continuesBackspacing(*last, change): last.old[0] = concat(change.old[0], last.old[0]) last.rng.Start = change.rng.Start // cursorBefore stays where the run of backspaces started, so undoing // the whole run puts the cursor back where the user began. last.cursorAfter = change.cursorAfter return true default: return false } } // continuesTyping reports whether change types straight on from where last // left off, on the same line. func continuesTyping(last, change edit) bool { return isSingleLine(last.new) && isSingleLine(change.new) && change.rng.Start == last.cursorAfter } // continuesBackspacing reports whether change deletes the character just // before the one last deleted, on the same line. func continuesBackspacing(last, change edit) bool { return isSingleLine(last.old) && isSingleLine(change.old) && change.rng.End == last.rng.Start } // clearHistory drops the undo and redo stacks, as loading a file does. func (b *Buffer) clearHistory() { b.undoStack = nil b.redoStack = nil b.coalesce = false } // isEmptyText reports whether lines hold no characters at all. Text is always // at least one line, so "empty" means one line of length zero. func isEmptyText(lines [][]rune) bool { return len(lines) == 1 && len(lines[0]) == 0 } // isSingleLine reports whether lines hold no line break. func isSingleLine(lines [][]rune) bool { return len(lines) == 1 }