github.com/aretext/aretext@v1.3.0/undo/log.go (about)

     1  package undo
     2  
     3  // LogEntry represents an entry in the undo log.
     4  type LogEntry struct {
     5  	Ops         []Op
     6  	CursorBegin uint64
     7  	CursorEnd   uint64
     8  }
     9  
    10  // Log tracks changes to a document and generates undo/redo operations.
    11  type Log struct {
    12  	stagedEntry          LogEntry
    13  	committedEntries     []LogEntry
    14  	numUndoEntries       int
    15  	numEntriesAtLastSave int
    16  }
    17  
    18  // NewLog constructs a new, empty undo log.
    19  func NewLog() *Log {
    20  	return &Log{
    21  		stagedEntry:          LogEntry{},
    22  		committedEntries:     nil,
    23  		numUndoEntries:       0,
    24  		numEntriesAtLastSave: 0,
    25  	}
    26  }
    27  
    28  // BeginEntry starts a new undo entry.
    29  // This should be called before tracking any operations.
    30  func (l *Log) BeginEntry(cursorPos uint64) {
    31  	l.stagedEntry = LogEntry{CursorBegin: cursorPos}
    32  }
    33  
    34  // CommitEntry completes an undo entry.
    35  // This should be called after BeginEntry.
    36  // If no operations were tracked, this does nothing.
    37  func (l *Log) CommitEntry(cursorPos uint64) {
    38  	if len(l.stagedEntry.Ops) == 0 {
    39  		return
    40  	}
    41  
    42  	if len(l.committedEntries) > l.numUndoEntries {
    43  		// Invalidate future changes.
    44  		l.committedEntries = l.committedEntries[0:l.numUndoEntries]
    45  	}
    46  
    47  	if l.numEntriesAtLastSave > l.numUndoEntries {
    48  		// Invalidate a save point in the future.
    49  		l.numEntriesAtLastSave = -1
    50  	}
    51  
    52  	l.stagedEntry.CursorEnd = cursorPos
    53  	l.committedEntries = append(l.committedEntries, l.stagedEntry)
    54  	l.stagedEntry = LogEntry{}
    55  	l.numUndoEntries++
    56  }
    57  
    58  // TrackOp tracks a change to the document.
    59  // This appends a new, uncommitted change and invalidates any future changes.
    60  func (l *Log) TrackOp(op Op) {
    61  	// Stage a new undo entry.
    62  	l.stagedEntry.Ops = append(l.stagedEntry.Ops, op)
    63  }
    64  
    65  // TrackSave moves the savepoint to the current entry.
    66  func (l *Log) TrackSave() {
    67  	l.numEntriesAtLastSave = l.numUndoEntries
    68  }
    69  
    70  // UndoToLastCommitted returns operations to transform the document back to its state before the last entry.
    71  // It also moves the current position backwards in the log.
    72  func (l *Log) UndoToLastCommitted() (hasEntry bool, ops []Op, cursor uint64) {
    73  	if l.numUndoEntries == 0 {
    74  		return false, nil, 0
    75  	}
    76  
    77  	entry := l.committedEntries[l.numUndoEntries-1]
    78  	ops = make([]Op, 0, len(entry.Ops))
    79  	for i := len(entry.Ops) - 1; i >= 0; i-- {
    80  		ops = append(ops, entry.Ops[i].Inverse())
    81  	}
    82  
    83  	l.numUndoEntries--
    84  
    85  	return true, ops, entry.CursorBegin
    86  }
    87  
    88  // RedoToNextCommitted returns operations to to transform the document to its state after the next entry.
    89  // It also moves the current position forward in the log.
    90  func (l *Log) RedoToNextCommitted() (hasEntry bool, ops []Op, cursor uint64) {
    91  	if l.numUndoEntries == len(l.committedEntries) {
    92  		return false, nil, 0
    93  	}
    94  
    95  	entry := l.committedEntries[l.numUndoEntries]
    96  	ops = make([]Op, 0, len(entry.Ops))
    97  	ops = append(ops, entry.Ops...)
    98  
    99  	l.numUndoEntries++
   100  
   101  	return true, ops, entry.CursorEnd
   102  }
   103  
   104  // HasUnsavedChanges returns whether the log has unsaved changes.
   105  func (l *Log) HasUnsavedChanges() bool {
   106  	return l.numUndoEntries != l.numEntriesAtLastSave
   107  }