more efficient undo/redo
Now the bottleneck shifts to applying undo/redo in large files. But things should be snappy if you don't use the sluggish feature.
This commit is contained in:
parent
22817492a3
commit
4f76ea37d7
|
@ -6,9 +6,8 @@ http://akkartik.name/lines.html
|
|||
|
||||
* No support yet for Unicode graphemes spanning multiple codepoints.
|
||||
|
||||
* 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.
|
||||
* Undo/redo can be sluggish in large files. If things get sluggish, killing
|
||||
the process can lose data.
|
||||
|
||||
* The text cursor will always stay on the screen. This can have some strange
|
||||
implications:
|
||||
|
|
92
text.lua
92
text.lua
|
@ -132,6 +132,13 @@ function Text.clip_selection(line_index, apos, bpos)
|
|||
end
|
||||
|
||||
function Text.delete_selection()
|
||||
local minl,maxl = minmax(Selection1.line, Cursor1.line)
|
||||
local before = snapshot(minl, maxl)
|
||||
Text.delete_selection_without_undo()
|
||||
record_undo_event({before=before, after=snapshot(Cursor1.line)})
|
||||
end
|
||||
|
||||
function Text.delete_selection_without_undo()
|
||||
if Selection1.line == nil then return end
|
||||
-- min,max = sorted(Selection1,Cursor1)
|
||||
local minl,minp = Selection1.line,Selection1.pos
|
||||
|
@ -1170,34 +1177,6 @@ function test_undo_delete_text()
|
|||
App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
|
||||
end
|
||||
|
||||
function test_zzz_undo_load_test()
|
||||
print('\n\nTesting a 10KB file')
|
||||
-- create a large list of lines
|
||||
local lines = {}
|
||||
-- 10k characters, hundred lines, 2k words
|
||||
for i=1,100 do
|
||||
local line = ''
|
||||
for c=1,20 do
|
||||
line = line..'abcd '
|
||||
end
|
||||
end
|
||||
Lines = load_array(lines)
|
||||
-- perform 1000 mutations
|
||||
print('are the dots printing quickly and without any pauses?')
|
||||
for i=1,1000 do
|
||||
if i%50 == 0 then
|
||||
App.run_after_keychord('return')
|
||||
else
|
||||
App.run_after_textinput('a')
|
||||
end
|
||||
if i%10 == 0 then
|
||||
io.write(i)
|
||||
io.write(' ')
|
||||
io.flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Text.compute_fragments(line, line_width)
|
||||
--? print('compute_fragments', line_width)
|
||||
line.fragments = {}
|
||||
|
@ -1243,14 +1222,15 @@ end
|
|||
function Text.textinput(t)
|
||||
if love.mouse.isDown('1') then return end
|
||||
if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end
|
||||
local down = love.keyboard.isDown
|
||||
if Selection1.line then
|
||||
Text.delete_selection()
|
||||
end
|
||||
local before = snapshot(Cursor1.line)
|
||||
Text.insert_at_cursor(t)
|
||||
record_undo_event({before=before, after=snapshot(Cursor1.line)})
|
||||
end
|
||||
|
||||
function Text.insert_at_cursor(t)
|
||||
if Selection1.line then Text.delete_selection() end
|
||||
-- Collect what you did in an event that can be undone.
|
||||
local before = snapshot()
|
||||
local byte_offset
|
||||
if Cursor1.pos > 1 then
|
||||
byte_offset = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
|
||||
|
@ -1261,8 +1241,6 @@ function Text.insert_at_cursor(t)
|
|||
Lines[Cursor1.line].fragments = nil
|
||||
Lines[Cursor1.line].screen_line_starting_pos = nil
|
||||
Cursor1.pos = Cursor1.pos+1
|
||||
-- finalize undo event
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
end
|
||||
|
||||
-- Don't handle any keys here that would trigger love.textinput above.
|
||||
|
@ -1270,7 +1248,8 @@ function Text.keychord_pressed(chord)
|
|||
--? print(chord)
|
||||
--== shortcuts that mutate text
|
||||
if chord == 'return' then
|
||||
local before = snapshot()
|
||||
local before_line = Cursor1.line
|
||||
local before = snapshot(before_line)
|
||||
local byte_offset = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
|
||||
table.insert(Lines, Cursor1.line+1, {mode='text', data=string.sub(Lines[Cursor1.line].data, byte_offset)})
|
||||
local scroll_down = (Cursor_y + math.floor(15*Zoom)) > App.screen.height
|
||||
|
@ -1283,20 +1262,24 @@ function Text.keychord_pressed(chord)
|
|||
Screen_top1.line = Cursor1.line
|
||||
Text.scroll_up_while_cursor_on_screen()
|
||||
end
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
|
||||
elseif chord == 'tab' then
|
||||
local before = snapshot()
|
||||
local before = snapshot(Cursor1.line)
|
||||
Text.insert_at_cursor('\t')
|
||||
save_to_disk(Lines, Filename)
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
record_undo_event({before=before, after=snapshot(Cursor1.line)})
|
||||
elseif chord == 'backspace' then
|
||||
local before = snapshot()
|
||||
if Selection1.line then
|
||||
Text.delete_selection()
|
||||
save_to_disk(Lines, Filename)
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
return
|
||||
end
|
||||
local before
|
||||
if Cursor1.pos > 1 then
|
||||
before = snapshot(Cursor1.line)
|
||||
else
|
||||
before = snapshot(Cursor1.line-1, Cursor1.line)
|
||||
end
|
||||
if Cursor1.pos > 1 then
|
||||
local byte_start = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos-1)
|
||||
local byte_end = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
|
||||
|
@ -1328,15 +1311,19 @@ function Text.keychord_pressed(chord)
|
|||
end
|
||||
assert(Text.le1(Screen_top1, Cursor1))
|
||||
save_to_disk(Lines, Filename)
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
record_undo_event({before=before, after=snapshot(Cursor1.line)})
|
||||
elseif chord == 'delete' then
|
||||
local before = snapshot()
|
||||
if Selection1.line then
|
||||
Text.delete_selection()
|
||||
save_to_disk(Lines, Filename)
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
return
|
||||
end
|
||||
local before
|
||||
if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
|
||||
before = snapshot(Cursor1.line)
|
||||
else
|
||||
before = snapshot(Cursor1.line, Cursor1.line+1)
|
||||
end
|
||||
if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
|
||||
local byte_start = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
|
||||
local byte_end = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos+1)
|
||||
|
@ -1360,7 +1347,7 @@ function Text.keychord_pressed(chord)
|
|||
end
|
||||
end
|
||||
save_to_disk(Lines, Filename)
|
||||
record_undo_event({before=before, after=snapshot()})
|
||||
record_undo_event({before=before, after=snapshot(Cursor1.line)})
|
||||
-- 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
|
||||
|
@ -1370,9 +1357,7 @@ function Text.keychord_pressed(chord)
|
|||
Screen_top1 = deepcopy(src.screen_top)
|
||||
Cursor1 = deepcopy(src.cursor)
|
||||
Selection1 = deepcopy(src.selection)
|
||||
if src.lines then
|
||||
Lines = deepcopy(src.lines)
|
||||
end
|
||||
patch(Lines, event.after, event.before)
|
||||
end
|
||||
elseif chord == 'M-y' then
|
||||
local event = redo_event()
|
||||
|
@ -1381,15 +1366,7 @@ function Text.keychord_pressed(chord)
|
|||
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
|
||||
patch(Lines, event.before, event.after)
|
||||
end
|
||||
-- paste
|
||||
elseif chord == 'M-c' then
|
||||
|
@ -1403,10 +1380,13 @@ function Text.keychord_pressed(chord)
|
|||
love.system.setClipboardText(s)
|
||||
end
|
||||
elseif chord == 'M-v' then
|
||||
local before_line = Cursor1.line
|
||||
local before = snapshot(before_line)
|
||||
local s = love.system.getClipboardText()
|
||||
for _,code in utf8.codes(s) do
|
||||
Text.insert_at_cursor(utf8.char(code))
|
||||
end
|
||||
record_undo_event({before=before, after=snapshot(before_line, Cursor1.line)})
|
||||
--== shortcuts that move the cursor
|
||||
elseif chord == 'left' then
|
||||
if Selection1.line then
|
||||
|
|
37
undo.lua
37
undo.lua
|
@ -32,8 +32,16 @@ function redo_event()
|
|||
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()
|
||||
function snapshot(s,e)
|
||||
-- Snapshot everything by default, but subset if requested.
|
||||
if s == nil and e == nil then
|
||||
s = 1
|
||||
e = #Lines
|
||||
elseif e == nil then
|
||||
e = s
|
||||
end
|
||||
-- compare with App.initialize_globals
|
||||
local event = {
|
||||
screen_top=deepcopy(Screen_top1),
|
||||
|
@ -43,10 +51,13 @@ function snapshot()
|
|||
previous_drawing_mode=Previous_drawing_mode,
|
||||
zoom=Zoom,
|
||||
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 _,line in ipairs(Lines) do
|
||||
for i=s,e do
|
||||
local line = Lines[i]
|
||||
if line.mode == 'text' then
|
||||
table.insert(event.lines, {mode='text', data=line.data})
|
||||
elseif line.mode == 'drawing' then
|
||||
|
@ -64,6 +75,24 @@ function snapshot()
|
|||
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)
|
||||
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)
|
||||
for i=1,#to.lines do
|
||||
table.insert(lines, to.start_line+i-1, to.lines[i])
|
||||
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
|
||||
|
@ -76,3 +105,7 @@ function deepcopy(obj, seen)
|
|||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function minmax(a, b)
|
||||
return math.min(a,b), math.max(a,b)
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue