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 }