after much struggle, a brute-force undo
[?]
Jun 2, 2022, 10:45 PM
73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2ACDependencies
- [2]
AD34IX2Zcouple more tests - [3]
PHFWIFYKscroll on enter - [4]
SLLR6KKIbugfix for non-ASCII - [5]
DHI6IJCNselecting text and deleting selections - [*]
R5QXEHUIsomebody stop me - [*]
BULPIBEGbeginnings of a module for the text editor - [*]
XNFTJHC4split keyboard handling between Text and Drawing - [*]
H2DPLWMVsnapshot: wrapping long lines at word boundaries - [*]
PFT5Y2ZYmove - [*]
2RXZ3PGObeginning of a new approach to scroll+wrap - [*]
2ZYV7D3Whandle tab characters - [*]
CG3264MMmove - [*]
OTIBCAUJlove2d scaffold - [*]
AVTNUQYRbasic test-enabled framework - [*]
AVQ5MC5Dfinish uppercasing all globals - [*]
BLWAYPKVextract a module - [*]
HYEAFRZ2split mouse_pressed events between Text and Drawing - [*]
6DE7RBZ6move mouse_released events to Drawing - [*]
VHQCNMARseveral more modules - [*]
FS2ITYYHrecord a known issue - [*]
A2TQYJ6J. - [*]
IDGP4BJZnew known issue with drawings
Change contents
- file addition: undo.lua[7.2]
-- 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 - edit in text.lua at line 5
require 'undo' - edit in text.lua at line 1096
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 - edit in text.lua at line 1224
-- Collect what you did in an event that can be undone.local before = snapshot_everything() - edit in text.lua at line 1236
-- finalize undo eventrecord_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1245
local before = snapshot_everything() - edit in text.lua at line 1258
record_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1260
local before = snapshot_everything() - edit in text.lua at line 1263
record_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1265
local before = snapshot_everything() - edit in text.lua at line 1268
save_to_disk(Lines, Filename)record_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1303
record_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1305
local before = snapshot_everything() - edit in text.lua at line 1308
save_to_disk(Lines, Filename)record_undo_event({before=before, after=snapshot_everything()}) - edit in text.lua at line 1335
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 - edit in main.lua at line 68
-- undoHistory = {}Next_history = 1 - edit in main.lua at line 109
App.initialize_globals() -- in particular, forget all undo history - edit in drawing.lua at line 209
Drawing.before = snapshot_everything() - edit in drawing.lua at line 351
record_undo_event({before=Drawing.before, after=snapshot_everything()}) - edit in README.md at line 8[23.148][24.14]
* Undo is extremely inefficient in space. While this app is extremely unlikelyto lose the current state of a file at any moment, undo history is volatileand should be considered unstable.