diff options
-rw-r--r-- | README.md | 48 | ||||
-rw-r--r-- | edit.lua | 80 | ||||
-rw-r--r-- | main.lua | 1 | ||||
-rw-r--r-- | run.lua | 4 | ||||
-rw-r--r-- | run_tests.lua | 29 | ||||
-rw-r--r-- | select.lua | 54 | ||||
-rw-r--r-- | source.lua | 4 | ||||
-rw-r--r-- | text.lua | 129 | ||||
-rw-r--r-- | text_tests.lua | 643 | ||||
-rw-r--r-- | undo.lua | 110 |
10 files changed, 28 insertions, 1074 deletions
diff --git a/README.md b/README.md index 97160fd..56d4f2a 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. @@ -18,7 +18,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 @@ -47,12 +47,6 @@ 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. @@ -65,31 +59,21 @@ found anything amiss: http://akkartik.name/contact This repo is a fork of [lines.love](http://akkartik.name/lines.html), an editor for plain text where you can also seamlessly insert line drawings. -Updates to it can be downloaded from the following mirrors: - -* 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://github.com/akkartik/text.love -* https://codeberg.org/akkartik/text.love -* https://notabug.org/akkartik/text.love -* https://pagure.io/text.love +Its immediate upstream is [text.love](https://git.sr.ht/~akkartik/text.love), +a version without support for line drawings. Updates to it can be downloaded +from the following mirrors: + +* https://git.sr.ht/~akkartik/view.love +* https://repo.or.cz/view.love.git +* https://tildegit.org/akkartik/view.love +* https://git.tilde.institute/akkartik/view.love +* https://github.com/akkartik/view.love +* https://codeberg.org/akkartik/view.love +* https://notabug.org/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://git.sr.ht/~akkartik/view.love -- a stripped down version without - support for modifying files; useful starting point for some forks. -* https://git.sr.ht/~akkartik/pong.love -- a fairly minimal example app that - can edit and debug its own source code. -* https://git.sr.ht/~akkartik/template-live-editor -- a template for - building "free-wheeling" live programs (easy to fork, can be modified as - they run), with a text editor primitive. -* https://git.sr.ht/~akkartik/luaML.love -- a free-wheeling 'browser' for a - Lua-based markup language built as a live program. -* https://git.sr.ht/~akkartik/driver.love -- a programming environment for - modifying free-wheeling programs while they run. - ## Feedback [Most appreciated.](http://akkartik.name/contact) diff --git a/edit.lua b/edit.lua index 3f7ec34..9347534 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.getSourceBaseDirectory()..'/lines.txt', -- '/' should work even on Windows - next_save = nil, - - -- undo - history = {}, - next_history = 1, -- search search_term = nil, @@ -124,25 +119,9 @@ function edit.draw(State) end function edit.update(State, dt) - if State.next_save and State.next_save < Current_time then - save_to_disk(State) - State.next_save = nil - end -end - -function schedule_save(State) - if State.next_save == nil then - State.next_save = Current_time + 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) - -- give some time for the OS to flush everything to disk - love.timer.sleep(0.1) - end end function edit.mouse_press(State, x,y, mouse_button) @@ -205,11 +184,7 @@ function edit.text_input(State, t) State.search_term = State.search_term..t State.search_text = nil Text.search_next(State) - else - for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll - Text.text_input(State, t) end - schedule_save(State) end function edit.keychord_press(State, chord, key) @@ -264,34 +239,6 @@ function edit.keychord_press(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-a' then State.selection1 = {line=1, pos=1} @@ -301,33 +248,6 @@ function edit.keychord_press(State, chord, key) 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 3166e56..36a48ca 100644 --- a/main.lua +++ b/main.lua @@ -43,7 +43,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') elseif Current_app == 'source' then diff --git a/run.lua b/run.lua index 9af9604..c36660c 100644 --- a/run.lua +++ b/run.lua @@ -33,7 +33,7 @@ function run.initialize(arg) Text.redraw_all(Editor_state) end edit.check_locs(Editor_state) - 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]) @@ -117,7 +117,7 @@ function run.file_drop(file) file:close() Text.redraw_all(Editor_state) edit.check_locs(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 c5c4eff..bea2807 100644 --- a/run_tests.lua +++ b/run_tests.lua @@ -43,32 +43,3 @@ function test_drop_file() check_eq(Editor_state.lines[3].data, 'ghi', 'lines:3') edit.draw(Editor_state) end - -function test_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', 'check') -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/source.lua b/source.lua index a390ed3..efe1198 100644 --- a/source.lua +++ b/source.lua @@ -72,7 +72,7 @@ function source.initialize() Menu_status_bar_height = 5 + Editor_state.line_height + 5 Editor_state.top = Editor_state.top + Menu_status_bar_height Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height - love.window.setTitle('text.love - source') + love.window.setTitle('view.love - source') end -- environment for a mutable file @@ -218,7 +218,7 @@ function source.file_drop(file) Text.redraw_all(Editor_state) Editor_state.screen_top1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1} - love.window.setTitle('text.love - source') + love.window.setTitle('view.love - source') end -- a copy of source.file_drop when given a filename diff --git a/text.lua b/text.lua index 464ab85..d386ca8 100644 --- a/text.lua +++ b/text.lua @@ -145,128 +145,10 @@ function Text.compute_fragments(State, line_index) end end -function Text.text_input(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) - 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 text_input above. function Text.keychord_press(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 - State.screen_top1 = { - line=State.cursor1.line, - pos=Text.pos_at_start_of_screen_line(State, State.cursor1), - } - 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 @@ -350,15 +232,6 @@ function Text.keychord_press(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, 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 25fda74..05bb375 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -13,44 +13,6 @@ function test_initial_state() check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos') end -function test_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, '#lines') - check_eq(Editor_state.cursor1.line, 1, 'cursor') - check_eq(Editor_state.screen_top1.line, 1, 'screen_top') -end - -function test_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_text_input(Editor_state, 'a') - local y = Editor_state.top - App.screen.check(y, 'a', 'screen:1') -end - -function test_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() App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() @@ -568,45 +530,6 @@ function test_cursor_movement_without_shift_resets_selection() check_eq(Editor_state.lines[1].data, 'abc', 'data') end -function test_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_text_input(Editor_state, 'x') - -- selected text is deleted and replaced with the key - check_eq(Editor_state.lines[1].data, 'xbc', 'check') -end - -function test_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_press(Editor_state, 'd', 'd') - edit.text_input(Editor_state, 'D') - edit.key_release(Editor_state, 'd') - App.fake_key_release('lshift') - -- selected text is deleted and replaced with the key - check_nil(Editor_state.selection1.line, 'check') - check_eq(Editor_state.lines[1].data, 'Dbc', 'data') -end - function test_copy_does_not_reset_selection() -- display a line of text with a selection App.screen.init{width=75, height=80} @@ -625,161 +548,21 @@ function test_copy_does_not_reset_selection() check(Editor_state.selection1.line, 'check') end -function test_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', 'clipboard') - -- selected text is deleted - check_eq(Editor_state.lines[1].data, 'bc', 'data') -end - -function test_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', 'check') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', '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, 'check') - check_eq(Editor_state.lines[1].data, 'ahi', 'data') -end - -function test_edit_wrapping_text() +function test_move_cursor_using_mouse() 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_text_input(Editor_state, 'g') - local y = Editor_state.top - App.screen.check(y, 'abc', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'de', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'fg', 'screen:3') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', '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, 'screen_top') - check_eq(Editor_state.cursor1.line, 2, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'a', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'bc', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'screen:3') -end - -function test_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, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - check_eq(Editor_state.lines[1].data, '', 'data:1') - check_eq(Editor_state.lines[2].data, 'abc', 'data:2') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', '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, 'screen_top') - check_eq(Editor_state.cursor1.line, 2, 'cursor:line') + Editor_state.selection1 = {} + edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache + edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1) + check_eq(Editor_state.cursor1.line, 1, 'cursor:line') check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'axy', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'zbc', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'screen:3') + check_nil(Editor_state.selection1.line, 'selection:line') + check_nil(Editor_state.selection1.pos, 'selection:pos') end function test_select_text_using_mouse() @@ -856,23 +639,6 @@ function test_select_text_repeatedly_using_mouse_and_shift() check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') end -function test_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, 'check') -end - function test_pagedown() App.screen.init{width=120, height=45} Editor_state = edit.initialize_test_state() @@ -1342,108 +1108,6 @@ function test_pageup_scrolls_up_from_middle_screen_line() App.screen.check(y, 'ghi ', 'screen:3') end -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', '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, 'screen_top') - check_eq(Editor_state.cursor1.line, 4, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'def', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'g', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'hi', 'screen:3') -end - -function test_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', '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, 'screen_top') - check_eq(Editor_state.cursor1.line, 5, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'j', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'screen:2') -end - -function test_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_text_input(Editor_state, 'a') - check_eq(Editor_state.screen_top1.line, 2, 'screen_top') - check_eq(Editor_state.cursor1.line, 2, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') - local y = Editor_state.top - App.screen.check(y, 'a', 'screen:1') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'baseline/screen:3') - -- after typing something the line wraps and the screen scrolls down - edit.run_after_text_input(Editor_state, 'j') - edit.run_after_text_input(Editor_state, 'k') - edit.run_after_text_input(Editor_state, 'l') - check_eq(Editor_state.screen_top1.line, 2, 'screen_top') - check_eq(Editor_state.cursor1.line, 3, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 7, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'def', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghij', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'screen:3') -end - function test_left_arrow_scrolls_up_in_wrapped_line() -- display lines starting from second screen line of a line App.screen.init{width=Editor_state.left+30, height=60} @@ -1562,299 +1226,6 @@ function test_end_scrolls_down_in_wrapped_line() App.screen.check(y, 'jkl', 'screen:3') end -function test_position_cursor_on_recently_edited_wrapping_line() - -- draw a line wrapping over 2 screen lines - 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 ', 'baseline1/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl mno pqr ', 'baseline1/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'baseline1/screen:3') - -- add to the line until it's wrapping over 3 screen lines - edit.run_after_text_input(Editor_state, 's') - edit.run_after_text_input(Editor_state, 't') - edit.run_after_text_input(Editor_state, 'u') - check_eq(Editor_state.cursor1.pos, 28, 'cursor:pos') - y = Editor_state.top - App.screen.check(y, 'abc def ghi ', 'baseline2/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl mno pqr ', 'baseline2/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'stu', '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, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 26, 'cursor:pos') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', '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, 'screen_top') - check_eq(Editor_state.cursor1.line, 1, 'cursor') - y = Editor_state.top - App.screen.check(y, 'abcdef', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'ghi', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'jkl', 'screen:3') -end - -function test_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', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'mno', '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', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'kl', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'mno', 'screen:3') - check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line') - check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos') - check_eq(Editor_state.cursor1.line, 3, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') -end - -function test_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', 'check') -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() - -- 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', 'data') - -- cursor (remains) at start of selection - check_eq(Editor_state.cursor1.line, 1, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - -- selection is cleared - check_nil(Editor_state.selection1.line, 'selection') -end - -function test_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', 'data') - -- cursor moves to start of selection - check_eq(Editor_state.cursor1.line, 1, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - -- selection is cleared - check_nil(Editor_state.selection1.line, 'selection') -end - -function test_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', 'data:1') - check_eq(Editor_state.lines[2].data, 'mno', 'data:2') - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 1, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') - -- selection is cleared - check_nil(Editor_state.selection1.line, 'selection') -end - -function test_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', 'data:1') - check_eq(Editor_state.lines[2].data, 'def', 'data:2') - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 1, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') - -- selection is cleared - check_nil(Editor_state.selection1.line, 'selection') -end - -function test_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', 'data:1') - check_eq(Editor_state.lines[2].data, 'f', 'data:2') - -- cursor remains at start of selection - check_eq(Editor_state.cursor1.line, 2, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos') - -- selection is cleared - check_nil(Editor_state.selection1.line, 'selection') -end - -function test_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_text_input(Editor_state, 'g') - check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line') - check_eq(Editor_state.cursor1.pos, 5, 'baseline/cursor:pos') - check_nil(Editor_state.selection1.line, 'baseline/selection:line') - check_nil(Editor_state.selection1.pos, 'baseline/selection:pos') - local y = Editor_state.top - App.screen.check(y, 'abc', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'defg', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'baseline/screen:3') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - check_eq(Editor_state.cursor1.line, 2, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') - check_nil(Editor_state.selection1.line, 'selection:line') - check_nil(Editor_state.selection1.pos, 'selection:pos') - y = Editor_state.top - App.screen.check(y, 'abc', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'screen:3') -end - -function test_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, 'baseline/cursor:line') - check_eq(Editor_state.cursor1.pos, 4, 'baseline/cursor:pos') - check_nil(Editor_state.selection1.line, 'baseline/selection:line') - check_nil(Editor_state.selection1.pos, 'baseline/selection:pos') - local y = Editor_state.top - App.screen.check(y, 'abc', 'baseline/screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'def', 'baseline/screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', '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, 'cursor:line') - check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos') - check_nil(Editor_state.selection1.line, 'selection:line') - check_nil(Editor_state.selection1.pos, 'selection:pos') ---? check_eq(Editor_state.selection1.line, 2, 'selection:line') ---? check_eq(Editor_state.selection1.pos, 4, 'selection:pos') - y = Editor_state.top - App.screen.check(y, 'abc', 'screen:1') - y = y + Editor_state.line_height - App.screen.check(y, 'defg', 'screen:2') - y = y + Editor_state.line_height - App.screen.check(y, 'xyz', 'screen:3') -end - -function test_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_text_input(Editor_state, 'x') - check_eq(Editor_state.lines[1].data, 'xbc', 'baseline') - check_nil(Editor_state.selection1.line, '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, 'line') - check_eq(Editor_state.selection1.pos, 2, 'pos') -end - function test_search() App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() 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 |