diff --git a/edit.lua b/edit.lua index e8a8f03..533408d 100644 --- a/edit.lua +++ b/edit.lua @@ -65,11 +65,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c width = right-left, filename = love.filesystem.getUserDirectory()..'/lines.txt', - next_save = nil, - - -- undo - history = {}, - next_history = 1, -- search search_term = nil, @@ -111,23 +106,9 @@ function edit.draw(State) end function edit.update(State, dt) - if State.next_save and State.next_save < App.getTime() then - save_to_disk(State) - State.next_save = nil - end -end - -function schedule_save(State) - if State.next_save == nil then - State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did - end end function edit.quit(State) - -- make sure to save before quitting - if State.next_save then - save_to_disk(State) - end end function edit.mouse_pressed(State, x,y, mouse_button) @@ -193,10 +174,7 @@ function edit.textinput(State, t) State.search_term = State.search_term..t State.search_text = nil Text.search_next(State) - else - Text.textinput(State, t) end - schedule_save(State) end function edit.keychord_pressed(State, chord, key) @@ -245,67 +223,12 @@ function edit.keychord_pressed(State, chord, key) elseif chord == 'C-0' then edit.update_font_settings(State, 20) Text.redraw_all(State) - -- undo - elseif chord == 'C-z' then - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - local event = undo_event(State) - if event then - local src = event.before - State.screen_top1 = deepcopy(src.screen_top) - State.cursor1 = deepcopy(src.cursor) - State.selection1 = deepcopy(src.selection) - patch(State.lines, event.after, event.before) - patch_placeholders(State.line_cache, event.after, event.before) - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) - schedule_save(State) - end - elseif chord == 'C-y' then - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - local event = redo_event(State) - if event then - local src = event.after - State.screen_top1 = deepcopy(src.screen_top) - State.cursor1 = deepcopy(src.cursor) - State.selection1 = deepcopy(src.selection) - patch(State.lines, event.before, event.after) - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) - schedule_save(State) - end -- clipboard elseif chord == 'C-c' then local s = Text.selection(State) if s then App.setClipboardText(s) end - elseif chord == 'C-x' then - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - local s = Text.cut_selection(State, State.left, State.right) - if s then - App.setClipboardText(s) - end - schedule_save(State) - elseif chord == 'C-v' then - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - -- We don't have a good sense of when to scroll, so we'll be conservative - -- and sometimes scroll when we didn't quite need to. - local before_line = State.cursor1.line - local before = snapshot(State, before_line) - local clipboard_data = App.getClipboardText() - for _,code in utf8.codes(clipboard_data) do - local c = utf8.char(code) - if c == '\n' then - Text.insert_return(State) - else - Text.insert_at_cursor(State, c) - end - end - if Text.cursor_out_of_screen(State) then - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) - end - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) -- dispatch to text else for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll diff --git a/main_tests.lua b/main_tests.lua index 31605f0..5c72064 100644 --- a/main_tests.lua +++ b/main_tests.lua @@ -44,33 +44,3 @@ function test_drop_file() check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3') edit.draw(Editor_state) end - -function test_drop_file_saves_previous() - io.write('\ntest_drop_file_saves_previous') - App.screen.init{width=Editor_state.left+300, height=300} - -- initially editing a file called foo that hasn't been saved to filesystem yet - Editor_state.lines = load_array{'abc', 'def'} - Editor_state.filename = 'foo' - schedule_save(Editor_state) - -- now drag a new file bar from the filesystem - App.filesystem['bar'] = 'abc\ndef\nghi\n' - local fake_dropped_file = { - opened = false, - getFilename = function(self) - return 'bar' - end, - open = function(self) - self.opened = true - end, - lines = function(self) - assert(self.opened) - return App.filesystem['bar']:gmatch('[^\n]+') - end, - close = function(self) - self.opened = false - end, - } - App.filedropped(fake_dropped_file) - -- filesystem now contains a file called foo - check_eq(App.filesystem['foo'], 'abc\ndef\n', 'F - test_drop_file_saves_previous') -end diff --git a/select.lua b/select.lua index b7cf65f..7baa398 100644 --- a/select.lua +++ b/select.lua @@ -95,60 +95,6 @@ function Text.to_pos(State, x,y) end end -function Text.cut_selection(State) - if State.selection1.line == nil then return end - local result = Text.selection(State) - Text.delete_selection(State) - return result -end - -function Text.delete_selection(State) - if State.selection1.line == nil then return end - local minl,maxl = minmax(State.selection1.line, State.cursor1.line) - local before = snapshot(State, minl, maxl) - Text.delete_selection_without_undo(State) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) -end - -function Text.delete_selection_without_undo(State) - if State.selection1.line == nil then return end - -- min,max = sorted(State.selection1,State.cursor1) - local minl,minp = State.selection1.line,State.selection1.pos - local maxl,maxp = State.cursor1.line,State.cursor1.pos - if minl > maxl then - minl,maxl = maxl,minl - minp,maxp = maxp,minp - elseif minl == maxl then - if minp > maxp then - minp,maxp = maxp,minp - end - end - -- update State.cursor1 and State.selection1 - State.cursor1.line = minl - State.cursor1.pos = minp - if Text.lt1(State.cursor1, State.screen_top1) then - State.screen_top1.line = State.cursor1.line - State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1) - end - State.selection1 = {} - -- delete everything between min (inclusive) and max (exclusive) - Text.clear_screen_line_cache(State, minl) - local min_offset = Text.offset(State.lines[minl].data, minp) - local max_offset = Text.offset(State.lines[maxl].data, maxp) - if minl == maxl then ---? print('minl == maxl') - State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset) - return - end - assert(minl < maxl) - local rhs = State.lines[maxl].data:sub(max_offset) - for i=maxl,minl+1,-1 do - table.remove(State.lines, i) - table.remove(State.line_cache, i) - end - State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs -end - function Text.selection(State) if State.selection1.line == nil then return end -- min,max = sorted(State.selection1,State.cursor1) diff --git a/text.lua b/text.lua index 0e79e8d..414f86c 100644 --- a/text.lua +++ b/text.lua @@ -3,7 +3,6 @@ Text = {} require 'search' require 'select' -require 'undo' require 'text_tests' -- draw a line starting from startpos to screen at y between State.left and State.right @@ -150,128 +149,11 @@ function Text.compute_fragments(State, line_index) end end -function Text.textinput(State, t) - if App.mouse_down(1) then return end - if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end - local before = snapshot(State, State.cursor1.line) ---? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) - Text.insert_at_cursor(State, t) - if State.cursor_y > App.screen.height - State.line_height then - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) ---? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) - end - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) -end - -function Text.insert_at_cursor(State, t) - local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset) - Text.clear_screen_line_cache(State, State.cursor1.line) - State.cursor1.pos = State.cursor1.pos+1 -end - -- Don't handle any keys here that would trigger love.textinput above. function Text.keychord_pressed(State, chord) --? print('chord', chord, State.selection1.line, State.selection1.pos) - --== shortcuts that mutate text - if chord == 'return' then - local before_line = State.cursor1.line - local before = snapshot(State, before_line) - Text.insert_return(State) - State.selection1 = {} - if State.cursor_y > App.screen.height - State.line_height then - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) - end - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - elseif chord == 'tab' then - local before = snapshot(State, State.cursor1.line) ---? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) - Text.insert_at_cursor(State, '\t') - if State.cursor_y > App.screen.height - State.line_height then - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) ---? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) - end - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - elseif chord == 'backspace' then - if State.selection1.line then - Text.delete_selection(State, State.left, State.right) - schedule_save(State) - return - end - local before - if State.cursor1.pos > 1 then - before = snapshot(State, State.cursor1.line) - local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1) - local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - if byte_start then - if byte_end then - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) - else - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) - end - State.cursor1.pos = State.cursor1.pos-1 - end - elseif State.cursor1.line > 1 then - before = snapshot(State, State.cursor1.line-1, State.cursor1.line) - -- join lines - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 - State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data - table.remove(State.lines, State.cursor1.line) - table.remove(State.line_cache, State.cursor1.line) - State.cursor1.line = State.cursor1.line-1 - end - if State.screen_top1.line > #State.lines then - Text.populate_screen_line_starting_pos(State, #State.lines) - local line_cache = State.line_cache[#State.line_cache] - State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]} - elseif Text.lt1(State.cursor1, State.screen_top1) then - local top2 = Text.to2(State, State.screen_top1) - top2 = Text.previous_screen_line(State, top2, State.left, State.right) - State.screen_top1 = Text.to1(State, top2) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks - end - Text.clear_screen_line_cache(State, State.cursor1.line) - assert(Text.le1(State.screen_top1, State.cursor1)) - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - elseif chord == 'delete' then - if State.selection1.line then - Text.delete_selection(State, State.left, State.right) - schedule_save(State) - return - end - local before - if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then - before = snapshot(State, State.cursor1.line) - else - before = snapshot(State, State.cursor1.line, State.cursor1.line+1) - end - if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then - local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1) - if byte_start then - if byte_end then - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end) - else - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1) - end - -- no change to State.cursor1.pos - end - elseif State.cursor1.line < #State.lines then - -- join lines - State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - table.remove(State.lines, State.cursor1.line+1) - table.remove(State.line_cache, State.cursor1.line+1) - end - Text.clear_screen_line_cache(State, State.cursor1.line) - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) --== shortcuts that move the cursor - elseif chord == 'left' then + if chord == 'left' then Text.left(State) State.selection1 = {} elseif chord == 'right' then @@ -355,16 +237,6 @@ function Text.keychord_pressed(State, chord) end end -function Text.insert_return(State) - local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) - table.insert(State.line_cache, State.cursor1.line+1, {}) - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1) - Text.clear_screen_line_cache(State, State.cursor1.line) - State.cursor1.line = State.cursor1.line+1 - State.cursor1.pos = 1 -end - function Text.pageup(State) --? print('pageup') -- duplicate some logic from love.draw diff --git a/text_tests.lua b/text_tests.lua index 7f9b2e4..0e6a2d7 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -14,47 +14,6 @@ function test_initial_state() check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos') end -function test_backspace_from_start_of_final_line() - io.write('\ntest_backspace_from_start_of_final_line') - -- display final line of text with cursor at start of it - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def'} - Editor_state.screen_top1 = {line=2, pos=1} - Editor_state.cursor1 = {line=2, pos=1} - Text.redraw_all(Editor_state) - -- backspace scrolls up - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(#Editor_state.lines, 1, 'F - test_backspace_from_start_of_final_line/#lines') - check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_from_start_of_final_line/cursor') - check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_from_start_of_final_line/screen_top') -end - -function test_insert_first_character() - io.write('\ntest_insert_first_character') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - edit.run_after_textinput(Editor_state, 'a') - local y = Editor_state.top - App.screen.check(y, 'a', 'F - test_insert_first_character/screen:1') -end - -function test_press_ctrl() - io.write('\ntest_press_ctrl') - -- press ctrl while the cursor is on text - App.screen.init{width=50, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{''} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.run_after_keychord(Editor_state, 'C-m') -end - function test_move_left() io.write('\ntest_move_left') App.screen.init{width=120, height=60} @@ -604,47 +563,6 @@ function test_cursor_movement_without_shift_resets_selection() check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data') end -function test_edit_deletes_selection() - io.write('\ntest_edit_deletes_selection') - -- display a line of text with some part selected - App.screen.init{width=75, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.selection1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- press a key - edit.run_after_textinput(Editor_state, 'x') - -- selected text is deleted and replaced with the key - check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection') -end - -function test_edit_with_shift_key_deletes_selection() - io.write('\ntest_edit_with_shift_key_deletes_selection') - -- display a line of text with some part selected - App.screen.init{width=75, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.selection1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- mimic precise keypresses for a capital letter - App.fake_key_press('lshift') - edit.keychord_pressed(Editor_state, 'd', 'd') - edit.textinput(Editor_state, 'D') - edit.key_released(Editor_state, 'd') - App.fake_key_release('lshift') - -- selected text is deleted and replaced with the key - check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection') - check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data') -end - function test_copy_does_not_reset_selection() io.write('\ntest_copy_does_not_reset_selection') -- display a line of text with a selection @@ -664,170 +582,6 @@ function test_copy_does_not_reset_selection() check(Editor_state.selection1.line, 'F - test_copy_does_not_reset_selection') end -function test_cut() - io.write('\ntest_cut') - -- display a line of text with some part selected - App.screen.init{width=75, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.selection1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- press a key - edit.run_after_keychord(Editor_state, 'C-x') - check_eq(App.clipboard, 'a', 'F - test_cut/clipboard') - -- selected text is deleted - check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data') -end - -function test_paste_replaces_selection() - io.write('\ntest_paste_replaces_selection') - -- display a line of text with a selection - App.screen.init{width=75, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=1} - Editor_state.selection1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- set clipboard - App.clipboard = 'xyz' - -- paste selection - edit.run_after_keychord(Editor_state, 'C-v') - -- selection is reset since shift key is not pressed - -- selection includes the newline, so it's also deleted - check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection') -end - -function test_deleting_selection_may_scroll() - io.write('\ntest_deleting_selection_may_scroll') - -- display lines 2/3/4 - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=2} - Editor_state.screen_top1 = {line=2, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'def', 'F - test_deleting_selection_may_scroll/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_deleting_selection_may_scroll/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', 'F - test_deleting_selection_may_scroll/baseline/screen:3') - -- set up a selection starting above the currently displayed page - Editor_state.selection1 = {line=1, pos=2} - -- delete selection - edit.run_after_keychord(Editor_state, 'backspace') - -- page scrolls up - check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll') - check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data') -end - -function test_edit_wrapping_text() - io.write('\ntest_edit_wrapping_text') - App.screen.init{width=50, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'xyz'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=4} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - edit.run_after_textinput(Editor_state, 'g') - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3') -end - -function test_insert_newline() - io.write('\ntest_insert_newline') - -- display a few lines - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3') - -- hitting the enter key splits the line - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_newline/screen_top') - check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline/cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'a', 'F - test_insert_newline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'bc', 'F - test_insert_newline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_insert_newline/screen:3') -end - -function test_insert_newline_at_start_of_line() - io.write('\ntest_insert_newline_at_start_of_line') - -- display a line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - -- hitting the enter key splits the line - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos') - check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1') - check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2') -end - -function test_insert_from_clipboard() - io.write('\ntest_insert_from_clipboard') - -- display a few lines - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3') - -- paste some text including a newline, check that new line is created - App.clipboard = 'xy\nz' - edit.run_after_keychord(Editor_state, 'C-v') - check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_from_clipboard/screen_top') - check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_from_clipboard/cursor:line') - check_eq(Editor_state.cursor1.pos, 2, 'F - test_insert_from_clipboard/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3') -end - function test_move_cursor_using_mouse() io.write('\ntest_move_cursor_using_mouse') App.screen.init{width=50, height=60} @@ -923,24 +677,6 @@ function test_select_text_repeatedly_using_mouse_and_shift() check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:pos') end -function test_cut_without_selection() - io.write('\ntest_cut_without_selection') - -- display a few lines - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - Editor_state.selection1 = {} - edit.draw(Editor_state) - -- try to cut without selecting text - edit.run_after_keychord(Editor_state, 'C-x') - -- no crash - check_nil(Editor_state.selection1.line, 'F - test_cut_without_selection') -end - function test_pagedown() io.write('\ntest_pagedown') App.screen.init{width=120, height=45} @@ -1427,112 +1163,6 @@ function test_pageup_scrolls_up_from_middle_screen_line() App.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3') end -function test_enter_on_bottom_line_scrolls_down() - io.write('\ntest_enter_on_bottom_line_scrolls_down') - -- display a few lines with cursor on bottom line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3') - -- after hitting the enter key the screen scrolls down - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.screen_top1.line, 2, 'F - test_enter_on_bottom_line_scrolls_down/screen_top') - check_eq(Editor_state.cursor1.line, 4, 'F - test_enter_on_bottom_line_scrolls_down/cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_bottom_line_scrolls_down/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3') -end - -function test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom() - io.write('\ntest_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom') - -- display just the bottom line on screen - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=4, pos=2} - Editor_state.screen_top1 = {line=4, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'jkl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/baseline/screen:1') - -- after hitting the enter key the screen does not scroll down - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.screen_top1.line, 4, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top') - check_eq(Editor_state.cursor1.line, 5, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2') -end - -function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom() - io.write('\ntest_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom') - -- display just an empty bottom line on screen - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', ''} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=1} - Editor_state.screen_top1 = {line=2, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- after hitting the inserting_text key the screen does not scroll down - edit.run_after_textinput(Editor_state, 'a') - check_eq(Editor_state.screen_top1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top') - check_eq(Editor_state.cursor1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line') - check_eq(Editor_state.cursor1.pos, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos') - local y = Editor_state.top - App.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1') -end - -function test_typing_on_bottom_line_scrolls_down() - io.write('\ntest_typing_on_bottom_line_scrolls_down') - -- display a few lines with cursor on bottom line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=4} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:3') - -- after typing something the line wraps and the screen scrolls down - edit.run_after_textinput(Editor_state, 'j') - edit.run_after_textinput(Editor_state, 'k') - edit.run_after_textinput(Editor_state, 'l') - check_eq(Editor_state.screen_top1.line, 2, 'F - test_typing_on_bottom_line_scrolls_down/screen_top') - check_eq(Editor_state.cursor1.line, 3, 'F - test_typing_on_bottom_line_scrolls_down/cursor:line') - check_eq(Editor_state.cursor1.pos, 7, 'F - test_typing_on_bottom_line_scrolls_down/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3') -end - function test_left_arrow_scrolls_up_in_wrapped_line() io.write('\ntest_left_arrow_scrolls_up_in_wrapped_line') -- display lines starting from second screen line of a line @@ -1655,311 +1285,6 @@ function test_end_scrolls_down_in_wrapped_line() App.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3') end -function test_position_cursor_on_recently_edited_wrapping_line() - -- draw a line wrapping over 2 screen lines - io.write('\ntest_position_cursor_on_recently_edited_wrapping_line') - App.screen.init{width=100, height=200} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=25} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:3') - -- add to the line until it's wrapping over 3 screen lines - edit.run_after_textinput(Editor_state, 's') - edit.run_after_textinput(Editor_state, 't') - edit.run_after_textinput(Editor_state, 'u') - check_eq(Editor_state.cursor1.pos, 28, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos') - y = Editor_state.top - App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3') - -- try to move the cursor earlier in the third screen line by clicking the mouse - edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1) - -- cursor should move - check_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line') - check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos') -end - -function test_backspace_can_scroll_up() - io.write('\ntest_backspace_can_scroll_up') - -- display the lines 2/3/4 with the cursor on line 2 - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=1} - Editor_state.screen_top1 = {line=2, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3') - -- after hitting backspace the screen scrolls up by one line - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_can_scroll_up/screen_top') - check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_can_scroll_up/cursor') - y = Editor_state.top - App.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3') -end - -function test_backspace_can_scroll_up_screen_line() - io.write('\ntest_backspace_can_scroll_up_screen_line') - -- display lines starting from second screen line of a line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=5} - Editor_state.screen_top1 = {line=3, pos=5} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:2') - -- after hitting backspace the screen scrolls up by one screen line - edit.run_after_keychord(Editor_state, 'backspace') - y = Editor_state.top - App.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3') - check_eq(Editor_state.screen_top1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/screen_top') - check_eq(Editor_state.screen_top1.pos, 1, 'F - test_backspace_can_scroll_up_screen_line/screen_top') - check_eq(Editor_state.cursor1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'F - test_backspace_can_scroll_up_screen_line/cursor:pos') -end - -function test_backspace_past_line_boundary() - io.write('\ntest_backspace_past_line_boundary') - -- position cursor at start of a (non-first) line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=1} - -- backspace joins with previous line - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary") -end - --- some tests for operating over selections created using Shift- chords --- we're just testing delete_selection, and it works the same for all keys - -function test_backspace_over_selection() - io.write('\ntest_backspace_over_selection') - -- select just one character within a line with cursor before selection - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.selection1 = {line=1, pos=2} - -- backspace deletes the selected character, even though it's after the cursor - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data") - -- cursor (remains) at start of selection - check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection/cursor:line") - check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos") - -- selection is cleared - check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection/selection") -end - -function test_backspace_over_selection_reverse() - io.write('\ntest_backspace_over_selection_reverse') - -- select just one character within a line with cursor after selection - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.selection1 = {line=1, pos=1} - -- backspace deletes the selected character - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data") - -- cursor moves to start of selection - check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection_reverse/cursor:line") - check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection_reverse/cursor:pos") - -- selection is cleared - check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection_reverse/selection") -end - -function test_backspace_over_multiple_lines() - io.write('\ntest_backspace_over_multiple_lines') - -- select just one character within a line with cursor after selection - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.selection1 = {line=4, pos=2} - -- backspace deletes the region and joins the remaining portions of lines on either side - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1") - check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2") - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_multiple_lines/cursor:line") - check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_over_multiple_lines/cursor:pos") - -- selection is cleared - check_nil(Editor_state.selection1.line, "F - test_backspace_over_multiple_lines/selection") -end - -function test_backspace_to_end_of_line() - io.write('\ntest_backspace_to_end_of_line') - -- select region from cursor to end of line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=2} - Editor_state.selection1 = {line=1, pos=4} - -- backspace deletes rest of line without joining to any other line - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1") - check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2") - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_to_start_of_line/cursor:line") - check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_to_start_of_line/cursor:pos") - -- selection is cleared - check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection") -end - -function test_backspace_to_start_of_line() - io.write('\ntest_backspace_to_start_of_line') - -- select region from cursor to start of line - App.screen.init{width=Editor_state.left+30, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=1} - Editor_state.selection1 = {line=2, pos=3} - -- backspace deletes beginning of line without joining to any other line - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1") - check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2") - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 2, "F - test_backspace_to_start_of_line/cursor:line") - check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_to_start_of_line/cursor:pos") - -- selection is cleared - check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection") -end - -function test_undo_insert_text() - io.write('\ntest_undo_insert_text') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'def', 'xyz'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=4} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - -- insert a character - edit.draw(Editor_state) - edit.run_after_textinput(Editor_state, 'g') - check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line') - check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos') - check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/baseline/selection:line') - check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/baseline/selection:pos') - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos') - check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/selection:line') - check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/selection:pos') - y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2') - y = y + Editor_state.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} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', 'defg', 'xyz'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=2, pos=5} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - -- delete a character - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos') - check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/baseline/selection:line') - check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/baseline/selection:pos') - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3') - -- undo ---? -- after undo, the backspaced key is selected - edit.run_after_keychord(Editor_state, 'C-z') - check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/cursor:line') - check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos') - check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/selection:line') - check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/selection:pos') ---? check_eq(Editor_state.selection1.line, 2, 'F - test_undo_delete_text/selection:line') ---? check_eq(Editor_state.selection1.pos, 4, 'F - test_undo_delete_text/selection:pos') - y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3') -end - -function test_undo_restores_selection() - io.write('\ntest_undo_restores_selection') - -- display a line of text with some part selected - App.screen.init{width=75, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.selection1 = {line=1, pos=2} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - edit.draw(Editor_state) - -- delete selected text - edit.run_after_textinput(Editor_state, 'x') - check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline') - check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - edit.run_after_keychord(Editor_state, 'C-z') - -- selection is restored - check_eq(Editor_state.selection1.line, 1, 'F - test_undo_restores_selection/line') - check_eq(Editor_state.selection1.pos, 2, 'F - test_undo_restores_selection/pos') -end - function test_search() io.write('\ntest_search') App.screen.init{width=120, height=60} diff --git a/undo.lua b/undo.lua deleted file mode 100644 index 912c949..0000000 --- a/undo.lua +++ /dev/null @@ -1,110 +0,0 @@ --- 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) - if e == nil then - e = s - end - assert(#State.lines > 0) - 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), - 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] - table.insert(event.lines, {data=line.data}) - 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) - 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 - -function patch_placeholders(line_cache, from, to) - assert(from.start_line == to.start_line) - 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) - 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