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 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
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
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