-- 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(State, data) State.history[State.next_history] = data State.next_history = State.next_history+1 for i=State.next_history,#State.history do State.history[i] = nil end end function undo_event(State) if State.next_history > 1 then --? print('moving to history', State.next_history-1) State.next_history = State.next_history-1 local result = State.history[State.next_history] return result end end function redo_event(State) if State.next_history <= #State.history then --? print('restoring history', State.next_history+1) local result = State.history[State.next_history] State.next_history = State.next_history+1 return result end end -- Copy all relevant global state. -- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories. function snapshot(State, s,e) -- Snapshot everything by default, but subset if requested. assert(s, 'failed to snapshot operation for undo history') if e == nil then e = s end assert(#State.lines > 0, 'failed to snapshot operation for undo history') if s < 1 then s = 1 end if s > #State.lines then s = #State.lines end if e < 1 then e = 1 end if e > #State.lines then e = #State.lines end -- compare with App.initialize_globals local event = { screen_top=deepcopy(State.screen_top1), selection=deepcopy(State.selection1), cursor=deepcopy(State.cursor1), current_drawing_mode=Drawing_mode, previous_drawing_mode=State.previous_drawing_mode, lines={}, start_line=s, end_line=e, -- no filename; undo history is cleared when filename changes } -- deep copy lines without cached stuff like text fragments for i=s,e do local line = State.lines[i] if line.mode == 'text' then table.insert(event.lines, {mode='text', data=line.data}) -- I've forgotten: should we deepcopy(line.data)? elseif line.mode == 'drawing' then table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}}) else assert(false, ('unknown line mode %s'):format(line.mode)) end end return event end function patch(lines, from, to) --? if #from.lines == 1 and #to.lines == 1 then --? assert(from.start_line == from.end_line) --? assert(to.start_line == to.end_line) --? assert(from.start_line == to.start_line) --? lines[from.start_line] = to.lines[1] --? return --? end assert(from.start_line == to.start_line, 'failed to patch undo operation') for i=from.end_line,from.start_line,-1 do table.remove(lines, i) end assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation') for i=1,#to.lines do table.insert(lines, to.start_line+i-1, to.lines[i]) end end function patch_placeholders(line_cache, from, to) assert(from.start_line == to.start_line, 'failed to patch undo operation') for i=from.end_line,from.start_line,-1 do table.remove(line_cache, i) end assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation') for i=1,#to.lines do table.insert(line_cache, to.start_line+i-1, {}) end 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 function minmax(a, b) return math.min(a,b), math.max(a,b) end