Incredibly inefficient, but I don't yet know how to efficiently encode undo mutations that can span multiple lines.
There seems to be one bug related to creating new drawings; they're not spawning events and undoing past drawing creation has some weird artifacts. Redo seems to consistently work, though.
73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC PHFWIFYKFOGVX7CEAMGJ3FDY6LL5QSZ7T7CTCZ66WMNXV6C242FAC SLLR6KKIAAJJPODFJLHXNG7Z22C3QUBGEIESWOFOGQVHYJJQ6VSQC DHI6IJCNSTHGED67T6H5X6Y636C7PIDGIJD32HBEKLT5WIMRS5MAC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC H2DPLWMVRFYTO2CQTG54FMT2LF3B6UKLXH32CUA22DNQJVP5XBNQC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC PFT5Y2ZYGQA6XXOZ5HH75WVUGA4B3KTDRHSFOZRAUKTPSFOPMNRAC 2RXZ3PGOTTZ6M4R372JXIKPLBQKPVBMAXNPIEO2HZDN4EMYW4GNAC 2ZYV7D3W2HPQW2HYB7XDPM4T7KEWPUFPZ77BDLCCDSCLRPJFK6PQC CG3264MMJTTSCJWUA2EMTBOPTDB2NZIJ7XICKHWUTZ4UWLFP7POAC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC AVQ5MC5DWNLI6LUUIPGBLGP4LKRPGWBY4THNY25OBT2FAVHC6MCAC HYEAFRZ2UEKDYTAE2GDQLHEJBPQASP2NDLMXB7F6MTVK2BKOXKEAC BLWAYPKV3MLDZ4ALXLUJ25AIR6PCIL4RFYNRYLB26GFVC2KQBYBAC 6DE7RBZ6RHNEICJ7EUMCTROK43LW4LYINULIF2QEQOKCXWLUYUXAC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC A2TQYJ6JZJF2T47C26H2IRSR6O67BP6VHY5PV7GTFG4IZNQQBJVQC IDGP4BJZTKAD6ZO4RLAWYVN6IFCMIM76G6HJGPTE27K4D6CDBUHQC FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC -- undo/redo by managing the sequence of events in the current session-- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu-- Incredibly inefficient; we make a copy of lines on every single keystroke.-- The hope here is that we're either editing small files or just reading large files.-- TODO: highlight stuff inserted by any undo/redo operation-- TODO: coalesce multiple similar operationsfunction record_undo_event(data)History[Next_history] = dataNext_history = Next_history+1for i=Next_history,#History doHistory[i] = nilendendfunction undo_event()if Next_history > 1 then--? print('moving to history', Next_history-1)Next_history = Next_history-1local result = History[Next_history]return resultendendfunction redo_event()if Next_history <= #History then--? print('restoring history', Next_history+1)local result = History[Next_history]Next_history = Next_history+1return resultendend-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.function snapshot_everything()-- compare with App.initialize_globalslocal event = {screen_top=deepcopy(Screen_top1),selection=deepcopy(Selection1),cursor=deepcopy(Cursor1),current_drawing_mode=Drawing_mode,previous_drawing_mode=Previous_drawing_mode,zoom=Zoom,lines={},-- no filename; undo history is cleared when filename changes}-- deep copy lines without cached stuff like text fragmentsfor _,line in ipairs(Lines) doif line.mode == 'text' thentable.insert(event.lines, {mode='text', data=line.data})elseif line.mode == 'drawing' thenlocal points=deepcopy(line.points)--? print('copying', line.points, 'with', #line.points, 'points into', points)local shapes=deepcopy(line.shapes)--? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)table.insert(event.lines, {mode='drawing', y=line.y, h=line.h, points=points, shapes=shapes, pending={}})--? table.insert(event.lines, {mode='drawing', y=line.y, h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})elseprint(line.mode)assert(false)endendreturn eventend-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080function deepcopy(obj, seen)if type(obj) ~= 'table' then return obj endif seen and seen[obj] then return seen[obj] endlocal s = seen or {}local result = setmetatable({}, getmetatable(obj))s[obj] = resultfor k,v in pairs(obj) doresult[deepcopy(k, s)] = deepcopy(v, s)endreturn resultend
function test_undo_insert_text()io.write('\ntest_undo_insert_text')App.screen.init{width=120, height=60}Lines = load_array{'abc', 'def', 'xyz'}Line_width = App.screen.widthCursor1 = {line=2, pos=4}Screen_top1 = {line=1, pos=1}Screen_bottom1 = {}Zoom = 1-- insert a characterApp.run_after_textinput('g')check_eq(Cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line')check_eq(Cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos')check_nil(Selection1.line, 'F - test_undo_insert_text/baseline/selection:line')check_nil(Selection1.pos, 'F - test_undo_insert_text/baseline/selection:pos')local screen_top_margin = 15 -- pixelslocal line_height = 15 -- pixelslocal y = screen_top_marginApp.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')y = y + line_heightApp.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')y = y + line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')-- undoApp.run_after_keychord('M-z')check_eq(Cursor1.line, 2, 'F - test_undo_insert_text/cursor:line')check_eq(Cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos')check_nil(Selection1.line, 'F - test_undo_insert_text/selection:line')check_nil(Selection1.pos, 'F - test_undo_insert_text/selection:pos')y = screen_top_marginApp.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')y = y + line_heightApp.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')y = y + line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')endfunction test_undo_delete_text()io.write('\ntest_undo_delete_text')App.screen.init{width=120, height=60}Lines = load_array{'abc', 'defg', 'xyz'}Line_width = App.screen.widthCursor1 = {line=2, pos=5}Screen_top1 = {line=1, pos=1}Screen_bottom1 = {}Zoom = 1-- delete a characterApp.run_after_keychord('backspace')check_eq(Cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line')check_eq(Cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos')check_nil(Selection1.line, 'F - test_undo_delete_text/baseline/selection:line')check_nil(Selection1.pos, 'F - test_undo_delete_text/baseline/selection:pos')local screen_top_margin = 15 -- pixelslocal line_height = 15 -- pixelslocal y = screen_top_marginApp.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')y = y + line_heightApp.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')y = y + line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')-- undo--? -- after undo, the backspaced key is selectedApp.run_after_keychord('M-z')check_eq(Cursor1.line, 2, 'F - test_undo_delete_text/cursor:line')check_eq(Cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos')check_nil(Selection1.line, 'F - test_undo_delete_text/selection:line')check_nil(Selection1.pos, 'F - test_undo_delete_text/selection:pos')--? check_eq(Selection1.line, 2, 'F - test_undo_delete_text/selection:line')--? check_eq(Selection1.pos, 4, 'F - test_undo_delete_text/selection:pos')y = screen_top_marginApp.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')y = y + line_heightApp.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')y = y + line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')end
record_undo_event({before=before, after=snapshot_everything()})-- undo/redo really belongs in main.lua, but it's here so I can test the-- text-specific portions of itelseif chord == 'M-z' thenlocal event = undo_event()if event thenlocal src = event.beforeScreen_top1 = deepcopy(src.screen_top)Cursor1 = deepcopy(src.cursor)Selection1 = deepcopy(src.selection)if src.lines thenLines = deepcopy(src.lines)endendelseif chord == 'M-y' thenlocal event = redo_event()if event thenlocal src = event.afterScreen_top1 = deepcopy(src.screen_top)Cursor1 = deepcopy(src.cursor)Selection1 = deepcopy(src.selection)if src.lines thenLines = deepcopy(src.lines)--? for _,line in ipairs(Lines) do--? if line.mode == 'drawing' then--? print('restoring', line.points, 'with', #line.points, 'points')--? print('restoring', line.shapes, 'with', #line.shapes, 'shapes')--? end--? endendend