1 /** 2 Copyright: Copyright (c) 2014-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module voxelman.gui.textedit.undostack; 8 9 import voxelman.container.chunkedbuffer; 10 11 struct UndoStack(Item) 12 { 13 private ChunkedBuffer!Item _undoStack; 14 private ChunkedBuffer!Item _redoStack; 15 private bool _currentGroup; 16 private bool _isGrouping = false; 17 18 void beginGroup() 19 { 20 _currentGroup = !_currentGroup; 21 _isGrouping = true; 22 } 23 24 void endGroup() 25 { 26 _isGrouping = false; 27 } 28 29 void commitUndoItem(Item item) 30 { 31 if (!_isGrouping) 32 { 33 _currentGroup = !_currentGroup; 34 } 35 36 item.group = _currentGroup; 37 _undoStack.putBack(item); 38 _redoStack.clear(); 39 } 40 41 void undo(scope Item delegate(Item) applyUndo) 42 { 43 undoRedoImpl(_undoStack, _redoStack, applyUndo); 44 } 45 46 void redo(scope Item delegate(Item) applyUndo) 47 { 48 undoRedoImpl(_redoStack, _undoStack, applyUndo); 49 } 50 51 void clear() 52 { 53 _undoStack.clear(); 54 _redoStack.clear(); 55 } 56 57 @property size_t undoSize() { return _undoStack.length; } 58 @property size_t redoSize() { return _redoStack.length; } 59 60 private void undoRedoImpl( 61 ref ChunkedBuffer!Item fromStack, 62 ref ChunkedBuffer!Item toStack, 63 scope Item delegate(Item) applyUndo) 64 { 65 if (fromStack.length == 0) return; 66 67 bool group = fromStack.back.group; 68 69 while (!fromStack.empty && fromStack.back.group == group) 70 { 71 // Get item to restore 72 Item undoItem = fromStack.back; 73 fromStack.removeBack(); 74 75 // Restore state 76 Item redoItem = applyUndo(undoItem); 77 78 // Save current state 79 toStack.putBack(redoItem); 80 } 81 82 if (!_undoStack.empty) 83 _currentGroup = _undoStack.back.group; 84 } 85 } 86 87 // Test undo/redo. 88 unittest 89 { 90 import voxelman.gui.textedit.textbuffer; 91 PieceTable table = PieceTable("abcdef"); 92 static struct UndoItem 93 { 94 PieceRestoreRange pieceUndo; 95 // Piece ranges in one group have the same flag 96 bool group; 97 } 98 UndoStack!UndoItem undoStack; 99 100 UndoItem onUndoRedoAction(UndoItem undoItem) 101 { 102 auto pieceUndo = undoItem.pieceUndo.apply(table.pieces.length); 103 return UndoItem(pieceUndo, undoItem.group); 104 } 105 106 void getUndo(PieceRestoreRange pieceUndo) 107 { 108 undoStack.commitUndoItem(UndoItem(pieceUndo)); 109 } 110 111 //table[].writeln; 112 //table.pieces.writeln; 113 114 assert(undoStack.undoSize == 0); 115 116 getUndo(table.remove(2, 2)); 117 assert(table[].equalDchars("abef")); 118 //table[].writeln; 119 //table.pieces.writeln; 120 121 assert(undoStack.undoSize == 1); 122 123 undoStack.undo(&onUndoRedoAction); 124 assert(table[].equalDchars("abcdef")); 125 //table[].writeln; 126 //table.pieces.writeln; 127 128 assert(undoStack.undoSize == 0); 129 assert(undoStack.redoSize == 1); 130 131 undoStack.redo(&onUndoRedoAction); 132 //table[].writeln; 133 //table.pieces.writeln; 134 assert(table[].equalDchars("abef")); 135 136 137 table = PieceTable("abcdef"); 138 undoStack.clear; 139 //table[].writeln; 140 141 getUndo(table.insert(2, "qw")); 142 assert(undoStack.undoSize == 1); 143 144 undoStack.undo(&onUndoRedoAction); 145 assert(table[].equalDchars("abcdef")); 146 undoStack.redo(&onUndoRedoAction); 147 assert(table[].equalDchars("abqwcdef")); 148 149 150 // Test undo/redo grouping. 151 table = PieceTable("абвгде"); 152 undoStack.clear; 153 154 undoStack.beginGroup(); 155 getUndo(table.insert(4, "12")); 156 getUndo(table.remove(6, 4)); 157 undoStack.endGroup(); 158 assert(table[].equalDchars("аб12де")); 159 160 undoStack.beginGroup(); 161 getUndo(table.insert(4, "12")); 162 getUndo(table.remove(4, 2)); 163 undoStack.endGroup(); 164 assert(table[].equalDchars("аб12де")); 165 166 getUndo(table.remove(4, 2)); 167 assert(table[].equalDchars("абде")); 168 169 undoStack.beginGroup(); 170 getUndo(table.insert(4, "12")); 171 undoStack.endGroup(); 172 assert(table[].equalDchars("аб12де")); 173 174 undoStack.undo(&onUndoRedoAction); 175 assert(table[].equalDchars("абде")); 176 177 undoStack.undo(&onUndoRedoAction); 178 assert(table[].equalDchars("аб12де")); 179 180 undoStack.undo(&onUndoRedoAction); 181 assert(table[].equalDchars("аб12де")); 182 183 undoStack.undo(&onUndoRedoAction); 184 assert(table[].equalDchars("абвгде")); 185 186 187 // Test redo discarding 188 table = PieceTable("абвгде"); 189 undoStack.clear(); 190 191 undoStack.beginGroup(); 192 getUndo(table.insert(0, "a")); 193 getUndo(table.insert(0, "a")); 194 getUndo(table.insert(0, "a")); 195 getUndo(table.insert(0, "a")); 196 undoStack.endGroup(); 197 198 undoStack.undo(&onUndoRedoAction); 199 assert(undoStack._redoStack.length == 4); 200 201 getUndo(table.insert(0, "a")); 202 assert(undoStack._redoStack.length == 0); 203 }