1 /** 2 Copyright: Copyright (c) 2014 Andrey Penechko. 3 License: a$(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module emulator.utils.undoproxy; 8 9 //version = debug_observer; 10 11 import std.algorithm : sort, SwapStrategy, filter, map; 12 import std.array : array, RefAppender, Appender, appender; 13 import std.range : iota, take, popFrontN; 14 import std.bitmanip : append, peek; 15 version(debug_observer) import std.stdio; 16 import std..string : format; 17 18 import emulator.utils.groupsequence; 19 20 @trusted: 21 22 /// Needs ubyte[] observableArray; in insertion context to work. 23 mixin template UndoHelper() 24 { 25 enum maxUndoSeqLength = ubyte.max - 1; 26 enum emptyFrameMarker = ubyte.max; 27 28 static if (observableArray.length <= ubyte.max) 29 { 30 alias PositionType = ubyte; 31 } 32 else static if (observableArray.length <= ushort.max) 33 { 34 alias PositionType = ushort; 35 } 36 else static if (observableArray.length <= uint.max) 37 { 38 alias PositionType = uint; 39 } 40 41 // Saves initial value of ubyte at index 42 ubyte[size_t] frameUndoMap; 43 44 Appender!(ubyte[]) undoStack; 45 46 ulong numUndoFrames; 47 48 49 bool hasUncommitedChanges() @property 50 { 51 return frameUndoMap.length > 0; 52 } 53 54 void discardFrame() 55 { 56 frameUndoMap = null; 57 } 58 59 void commitFrame(ulong frameNumber) 60 { 61 scope(exit) ++numUndoFrames; 62 63 // Compress empty undos 64 if (frameUndoMap.length == 0) 65 { 66 commitEmptyFrame(); 67 return; 68 } 69 70 auto changeGroups = frameUndoMap 71 .keys() 72 .sort!("a<b", SwapStrategy.stable) 73 .filter!(a => frameUndoMap[a] != observableArray[a]) 74 .groupSequence!"b-a == 1"; //[0] position, [1] count 75 76 undoStack.append!ubyte(0); // Frame separator; 77 78 foreach(changeGroup; changeGroups) 79 { 80 PositionType position = cast(PositionType)changeGroup[0]; 81 82 size_t len = changeGroup[1]; 83 84 if (len <= maxUndoSeqLength) 85 { 86 undoStack ~= iota(position, position+len).map!(a => frameUndoMap[a]); 87 undoStack.append!PositionType(position); 88 undoStack.append!ubyte(cast(ubyte)len); 89 } 90 else 91 { 92 auto undoElements = iota(position, position+len).map!(a => frameUndoMap[a]); 93 94 while (!undoElements.empty) 95 { 96 auto numElementsToAdd = undoElements.length > maxUndoSeqLength ? 97 maxUndoSeqLength : undoElements.length; 98 99 undoStack ~= undoElements.take(numElementsToAdd); 100 undoStack.append!PositionType(position); 101 undoStack.append!ubyte(cast(ubyte)numElementsToAdd); 102 103 undoElements.popFrontN(numElementsToAdd); 104 105 position += numElementsToAdd; 106 } 107 } 108 } 109 110 discardFrame(); 111 } 112 113 // Adds empty frame or increases count of empty frames if one already exists on top of stack. 114 void commitEmptyFrame() 115 { 116 if (undoStackSize > 0) 117 { 118 ubyte length = undoStack.data.peek!ubyte(undoStackSize - 1); 119 120 if (length == emptyFrameMarker) 121 { 122 ubyte numEmptyFrames = undoStack.data.peek!ubyte(undoStackSize - 2); 123 124 if (numEmptyFrames < ubyte.max) 125 { 126 undoStack.data[undoStackSize - 2] = cast(ubyte)(numEmptyFrames + 1); 127 return; 128 } 129 } 130 } 131 132 // no empty frame on top of undo stack was found. 133 addEmptyUndoFrame(); 134 } 135 136 void addEmptyUndoFrame() 137 { 138 undoStack.append!ubyte(1); // Num empty frames. 139 undoStack.append!ubyte(emptyFrameMarker); // Marker. 140 } 141 142 private void addUndoAction(size_t pos, ubyte[] data) 143 in 144 { 145 assert((pos + data.length - 1) < observableArray.length); 146 } 147 body 148 { 149 foreach(index; pos..pos + data.length) 150 { 151 if (index !in frameUndoMap && observableArray[index] != data[index - pos]) 152 { 153 frameUndoMap[index] = observableArray[index]; 154 } 155 } 156 } 157 158 void discardUndoStack() 159 { 160 undoStack.shrinkTo(0); 161 numUndoFrames = 0; 162 } 163 164 void undoFrames(ulong numFrames = 1) 165 { 166 ulong framesUndone = 0; 167 168 while(undoStackSize > 0 && framesUndone < numFrames) 169 { 170 while(undoStackSize > 0) 171 { 172 // extract undo data length 173 ubyte length = undoStack.data.peek!ubyte(undoStackSize - 1); 174 175 // frame end found 176 if (length == 0) 177 { 178 undoStack.shrinkTo(undoStackSize - 1); 179 break; 180 } 181 else if (length == emptyFrameMarker) 182 { 183 ubyte numEmptyFrames = undoStack.data.peek!ubyte(undoStackSize - 2); 184 185 // Last empty frame 186 if (numEmptyFrames == 1) 187 { 188 undoStack.shrinkTo(undoStackSize - 2); 189 break; 190 } 191 192 // Undo one empty frame by decreasing counter. 193 undoStack.data[$ - 2] = cast(ubyte)(numEmptyFrames - 1); 194 195 break; 196 } 197 198 // extract target positon 199 size_t position = undoStack.data.peek!PositionType(undoStackSize - PositionType.sizeof - 1); 200 201 // extract undo data 202 auto undoData = undoStack.data[$ - (PositionType.sizeof + 1 + length)..$ - PositionType.sizeof - 1]; 203 204 // Make undo 205 observableArray[position..position + undoData.length] = undoData; 206 207 // Shrink undo stack 208 auto shrinkDelta = ubyte.sizeof + PositionType.sizeof + length; 209 undoStack.shrinkTo(undoStackSize - shrinkDelta); 210 } 211 212 ++framesUndone; 213 --numUndoFrames; 214 version(debug_observer) writefln("Undo stack %s\n", undoStack.data); 215 } 216 } 217 218 size_t undoStackSize() @property 219 { 220 return undoStack.data.length; 221 } 222 } 223 224 struct UndoableStruct(Struct, ArrayElement) 225 { 226 union 227 { 228 Struct data; 229 ubyte[Struct.sizeof] observableArray; 230 } 231 232 mixin UndoHelper; 233 234 void reset() 235 { 236 observableArray[] = 0; 237 discardFrame(); 238 discardUndoStack(); 239 } 240 241 auto opMemberAssign(string member, string op, T = ubyte)(T value = 1) 242 { 243 alias Member = typeof(__traits(getMember, data, member)); 244 245 return opDispatch!(member)( 246 cast(Member)(mixin("opDispatch!(member)() "~op~" value")) 247 ); 248 } 249 250 alias inc(string member) = opMemberAssign!(member, "+"); 251 alias dec(string member) = opMemberAssign!(member, "-"); 252 253 // setter 254 auto opDispatch(string member, T)(T newValue) 255 { 256 alias Member = typeof(__traits(getMember, data, member)); 257 258 auto value = __traits(getMember, data, member); 259 260 if (value == newValue) return value; 261 262 Member newValueCasted = cast(Member)newValue; 263 264 addUndoAction( 265 __traits(getMember, data, member).offsetof, 266 *(cast(ubyte[Member.sizeof]*)&newValueCasted) 267 ); 268 269 __traits(getMember, data, member) = newValueCasted; 270 271 return __traits(getMember, data, member); 272 } 273 274 // getter 275 auto opDispatch(string member)() 276 { 277 return __traits(getMember, data, member); 278 } 279 280 auto opIndex(size_t index) 281 { 282 return (cast(ArrayElement[])observableArray)[index]; 283 } 284 285 void opSliceAssign(ArrayElement[] data, size_t i, size_t j) 286 in 287 { 288 assert(j <= (cast(ArrayElement[])observableArray).length); 289 assert(i < j); 290 assert(data.length == j - i, format("Arrays have different sizes %s and %s", data.length, j - i)); 291 } 292 body 293 { 294 295 addUndoAction(i * ArrayElement.sizeof, cast(ubyte[])data); 296 297 (cast(ArrayElement[])observableArray)[i..j] = data; 298 } 299 300 ArrayElement opIndexAssign(ArrayElement data, size_t index) 301 { 302 assert(index <= (cast(ArrayElement[])observableArray).length); 303 304 addUndoAction(index * ArrayElement.sizeof, *(cast(ubyte[ArrayElement.sizeof]*)&data)); 305 306 return (cast(ArrayElement[])observableArray)[index] = data; 307 } 308 }