after much struggle, a brute-force undo

[?]
Jun 2, 2022, 10:45 PM
73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC

Dependencies

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 operations
    function record_undo_event(data)
    History[Next_history] = data
    Next_history = Next_history+1
    for i=Next_history,#History do
    History[i] = nil
    end
    end
    function undo_event()
    if Next_history > 1 then
    --? print('moving to history', Next_history-1)
    Next_history = Next_history-1
    local result = History[Next_history]
    return result
    end
    end
    function redo_event()
    if Next_history <= #History then
    --? print('restoring history', Next_history+1)
    local result = History[Next_history]
    Next_history = Next_history+1
    return result
    end
    end
    -- 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_globals
    local 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 fragments
    for _,line in ipairs(Lines) do
    if line.mode == 'text' then
    table.insert(event.lines, {mode='text', data=line.data})
    elseif line.mode == 'drawing' then
    local 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={}})
    else
    print(line.mode)
    assert(false)
    end
    end
    return event
    end
    -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
    function deepcopy(obj, seen)
    if type(obj) ~= 'table' then return obj end
    if seen and seen[obj] then return seen[obj] end
    local s = seen or {}
    local result = setmetatable({}, getmetatable(obj))
    s[obj] = result
    for k,v in pairs(obj) do
    result[deepcopy(k, s)] = deepcopy(v, s)
    end
    return result
    end
  • edit in text.lua at line 5
    [9.30]
    [10.1567]
    require 'undo'
  • edit in text.lua at line 1096
    [5.8374]
    [11.1]
    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.width
    Cursor1 = {line=2, pos=4}
    Screen_top1 = {line=1, pos=1}
    Screen_bottom1 = {}
    Zoom = 1
    -- insert a character
    App.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 -- pixels
    local line_height = 15 -- pixels
    local y = screen_top_margin
    App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')
    y = y + line_height
    App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')
    y = y + line_height
    App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')
    -- undo
    App.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_margin
    App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')
    y = y + line_height
    App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')
    y = y + line_height
    App.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')
    end
    function 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.width
    Cursor1 = {line=2, pos=5}
    Screen_top1 = {line=1, pos=1}
    Screen_bottom1 = {}
    Zoom = 1
    -- delete a character
    App.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 -- pixels
    local line_height = 15 -- pixels
    local y = screen_top_margin
    App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')
    y = y + line_height
    App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')
    y = y + line_height
    App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')
    -- undo
    --? -- after undo, the backspaced key is selected
    App.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_margin
    App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')
    y = y + line_height
    App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')
    y = y + line_height
    App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
    end
  • edit in text.lua at line 1224
    [4.55]
    [9.171]
    -- Collect what you did in an event that can be undone.
    local before = snapshot_everything()
  • edit in text.lua at line 1236
    [12.763]
    [9.513]
    -- finalize undo event
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1245
    [9.655]
    [12.764]
    local before = snapshot_everything()
  • edit in text.lua at line 1258
    [3.1648]
    [13.134]
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1260
    [13.163]
    [13.163]
    local before = snapshot_everything()
  • edit in text.lua at line 1263
    [9.1006]
    [9.2046]
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1265
    [9.2081]
    [5.8395]
    local before = snapshot_everything()
  • edit in text.lua at line 1268
    [5.8453]
    [5.8453]
    save_to_disk(Lines, Filename)
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1303
    [9.3034]
    [9.3034]
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1305
    [9.3066]
    [5.8475]
    local before = snapshot_everything()
  • edit in text.lua at line 1308
    [5.8533]
    [5.8533]
    save_to_disk(Lines, Filename)
    record_undo_event({before=before, after=snapshot_everything()})
  • edit in text.lua at line 1335
    [9.3956]
    [14.36]
    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 it
    elseif chord == 'M-z' then
    local event = undo_event()
    if event then
    local src = event.before
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    if src.lines then
    Lines = deepcopy(src.lines)
    end
    end
    elseif chord == 'M-y' then
    local event = redo_event()
    if event then
    local src = event.after
    Screen_top1 = deepcopy(src.screen_top)
    Cursor1 = deepcopy(src.cursor)
    Selection1 = deepcopy(src.selection)
    if src.lines then
    Lines = 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
    --? end
    end
    end
  • edit in main.lua at line 68
    [2.1568]
    [2.1568]
    -- undo
    History = {}
    Next_history = 1
  • edit in main.lua at line 109
    [16.1352]
    [17.747]
    App.initialize_globals() -- in particular, forget all undo history
  • edit in drawing.lua at line 209
    [19.853]
    [19.853]
    Drawing.before = snapshot_everything()
  • edit in drawing.lua at line 351
    [20.3159]
    [21.19072]
    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 unlikely
    to lose the current state of a file at any moment, undo history is volatile
    and should be considered unstable.