diff options
-rw-r--r-- | README.md | 45 | ||||
-rw-r--r-- | edit.lua | 77 | ||||
-rw-r--r-- | main.lua | 5 | ||||
-rw-r--r-- | run.lua | 4 | ||||
-rw-r--r-- | run_tests.lua | 30 | ||||
-rw-r--r-- | select.lua | 54 | ||||
-rw-r--r-- | text.lua | 130 | ||||
-rw-r--r-- | text_tests.lua | 675 | ||||
-rw-r--r-- | undo.lua | 110 |
9 files changed, 21 insertions, 1109 deletions
diff --git a/README.md b/README.md index 991632b..eface47 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# An editor for plain text. +# A read-only viewer for plain text. Not very useful by itself, but it's a fork of [lines.love](http://akkartik.name/lines.html) -that you can take in other directions besides line drawings, while easily -sharing patches between forks. +that you can take in other directions, while easily sharing patches between +forks. Designed above all to be easy to modify and give you early warning if your modifications break something. @@ -14,7 +14,7 @@ optionally with a file path to edit. Alternatively, turn it into a .love file you can double-click on: ``` -$ zip -r /tmp/text.love *.lua +$ zip -r /tmp/view.love *.lua ``` By default, it reads/writes the file `lines.txt` in your default @@ -42,20 +42,10 @@ found anything amiss: http://akkartik.name/contact * No support yet for right-to-left languages. -* Undo/redo may be sluggish in large files. Large files may grow sluggish in - other ways. Works well in all circumstances with files under 50KB. - -* If you kill the process, say by force-quitting because things things get - sluggish, you can lose data. - * Long wrapping lines can't yet distinguish between the cursor at end of one screen line and start of the next, so clicking the mouse to position the cursor can very occasionally do the wrong thing. -* Touchpads can drag the mouse pointer using a light touch or a heavy click. - On Linux, drags using the light touch get interrupted when a key is pressed. - You'll have to press down to drag. - * Can't scroll while selecting text with mouse. * No scrollbars yet. That stuff is hard. @@ -67,24 +57,21 @@ found anything amiss: http://akkartik.name/contact ## Mirrors and Forks This repo is a fork of lines.love at [http://akkartik.name/lines.html](http://akkartik.name/lines.html). -Updates to it can be downloaded from the following mirrors: - -* https://codeberg.org/akkartik/text.love -* https://repo.or.cz/text.love.git -* https://tildegit.org/akkartik/text.love -* https://git.tilde.institute/akkartik/text.love -* https://git.sr.ht/~akkartik/text.love -* https://notabug.org/akkartik/text.love -* https://github.com/akkartik/text.love -* https://pagure.io/text.love +Its immediate upstream is [text.love](https://codeberg.org/akkartik/text.love), +a version without support for line drawings. Updates to it can be downloaded +from the following mirrors: + +* https://codeberg.org/akkartik/view.love +* https://repo.or.cz/view.love.git +* https://tildegit.org/akkartik/view.love +* https://git.tilde.institute/akkartik/view.love +* https://git.sr.ht/~akkartik/view.love +* https://notabug.org/akkartik/view.love +* https://github.com/akkartik/view.love +* https://pagure.io/view.love Further forks are encouraged. If you show me your fork, I'll link to it here. -* https://codeberg.org/akkartik/view.love -- a stripped down version without - support for modifying files; useful starting point for some forks. -* https://codeberg.org/akkartik/pong.love -- a fairly minimal example app that - can edit and debug its own source code. - ## Feedback [Most appreciated.](http://akkartik.name/contact) diff --git a/edit.lua b/edit.lua index 768313f..215f449 100644 --- a/edit.lua +++ b/edit.lua @@ -60,11 +60,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, @@ -107,23 +102,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) @@ -192,10 +173,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) @@ -244,67 +222,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.lua b/main.lua index 00a6af9..aadc273 100644 --- a/main.lua +++ b/main.lua @@ -42,7 +42,6 @@ function App.load() load_file_from_source_or_save_directory('text.lua') load_file_from_source_or_save_directory('search.lua') load_file_from_source_or_save_directory('select.lua') - load_file_from_source_or_save_directory('undo.lua') load_file_from_source_or_save_directory('text_tests.lua') load_file_from_source_or_save_directory('run_tests.lua') else @@ -83,7 +82,7 @@ function App.initialize(arg) else assert(false, 'unknown app "'..Current_app..'"') end - love.window.setTitle('text.love - '..Current_app) + love.window.setTitle('view.love - '..Current_app) end function App.resize(w,h) @@ -105,7 +104,7 @@ function App.filedropped(file) else assert(false, 'unknown app "'..Current_app..'"') end - love.window.setTitle('text.love - '..Current_app) + love.window.setTitle('view.love - '..Current_app) end function App.focus(in_focus) diff --git a/run.lua b/run.lua index d295afe..d780f3b 100644 --- a/run.lua +++ b/run.lua @@ -36,7 +36,7 @@ function run.initialize(arg) load_from_disk(Editor_state) Text.redraw_all(Editor_state) end - love.window.setTitle('text.love - '..Editor_state.filename) + love.window.setTitle('view.love - '..Editor_state.filename) if #arg > 1 then print('ignoring commandline args after '..arg[1]) @@ -112,7 +112,7 @@ function run.filedropped(file) Editor_state.lines = load_from_file(file) file:close() Text.redraw_all(Editor_state) - love.window.setTitle('text.love - '..Editor_state.filename) + love.window.setTitle('view.love - '..Editor_state.filename) end function run.draw() diff --git a/run_tests.lua b/run_tests.lua index 31605f0..5c72064 100644 --- a/run_tests.lua +++ b/run_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 9156498..2e71c68 100644 --- a/text.lua +++ b/text.lua @@ -1,6 +1,5 @@ -- text editor, particularly text drawing, horizontal wrap, vertical scrolling Text = {} -AB_padding = 20 -- space in pixels between A side and B side -- draw a line starting from startpos to screen at y between State.left and State.right -- return the final y, and position of start of final screen line drawn @@ -146,128 +145,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 @@ -351,16 +233,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 |