diff options
-rw-r--r-- | README.md | 46 | ||||
-rw-r--r-- | app.lua | 4 | ||||
-rw-r--r-- | conf.lua | 3 | ||||
-rw-r--r-- | drawing.lua | 6 | ||||
-rw-r--r-- | edit.lua | 80 | ||||
-rw-r--r-- | file.lua | 2 | ||||
-rw-r--r-- | keychord.lua | 4 | ||||
-rw-r--r-- | log.lua | 50 | ||||
-rw-r--r-- | log_browser.lua | 6 | ||||
-rw-r--r-- | main.lua | 28 | ||||
-rw-r--r-- | reference.md | 3 | ||||
-rw-r--r-- | run.lua | 20 | ||||
-rw-r--r-- | run_tests.lua | 29 | ||||
-rw-r--r-- | select.lua | 54 | ||||
-rw-r--r-- | source.lua | 30 | ||||
-rw-r--r-- | source_edit.lua | 26 | ||||
-rw-r--r-- | source_text.lua | 2 | ||||
-rw-r--r-- | source_text_tests.lua | 7 | ||||
-rw-r--r-- | source_undo.lua | 8 | ||||
-rw-r--r-- | text.lua | 137 | ||||
-rw-r--r-- | text_tests | 2 | ||||
-rw-r--r-- | text_tests.lua | 629 | ||||
-rw-r--r-- | undo.lua | 88 |
23 files changed, 148 insertions, 1116 deletions
diff --git a/README.md b/README.md index 0887f4a..bb4ab40 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# An editor for plain text. +# A read-only viewer for plain text. [](https://0dependencies.dev) 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. @@ -19,7 +19,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 @@ -56,33 +56,23 @@ 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.merveilles.town/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 -* https://nest.pijul.com/akkartik/text.love (using the Pijul version control system) +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://git.merveilles.town/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 +* https://nest.pijul.com/akkartik/view.love (using the Pijul version control system) 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) Messages, PRs, patches, diff --git a/app.lua b/app.lua index 1bec1f0..2375ff9 100644 --- a/app.lua +++ b/app.lua @@ -428,9 +428,9 @@ function App.disable_tests() -- love.keyboard.isDown doesn't work on Android, so emulate it using -- keypressed and keyreleased events if name == 'keypressed' then - love.handlers[name] = function(key, scancode, isrepeat) + love.handlers[name] = function(key, scancode, is_repeat) Keys_down[key] = true - return App.keypressed(key, scancode, isrepeat) + return App.keypressed(key, scancode, is_repeat) end elseif name == 'keyreleased' then love.handlers[name] = function(key, scancode) diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..d5b3903 --- /dev/null +++ b/conf.lua @@ -0,0 +1,3 @@ +function love.conf(t) + t.identity = 'view' +end diff --git a/drawing.lua b/drawing.lua index 92e3d5f..b74855a 100644 --- a/drawing.lua +++ b/drawing.lua @@ -221,7 +221,7 @@ function Drawing.in_drawing(State, line_index, x,y, left,right) return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right end -function Drawing.mouse_press(State, drawing_index, x,y, mouse_button) +function Drawing.mouse_press(State, drawing_index, x,y, mouse_button, is_touch, presses) local drawing = State.lines[drawing_index] local starty = Text.starty(State, drawing_index) local cx = Drawing.coord(x-State.left, State.width) @@ -300,7 +300,7 @@ function Drawing.relax_constraints(drawing, p) end end -function Drawing.mouse_release(State, x,y, mouse_button) +function Drawing.mouse_release(State, x,y, mouse_button, is_touch, presses) if State.current_drawing_mode == 'move' then State.current_drawing_mode = State.previous_drawing_mode State.previous_drawing_mode = nil @@ -396,7 +396,7 @@ function Drawing.mouse_release(State, x,y, mouse_button) end end -function Drawing.keychord_press(State, chord) +function Drawing.keychord_press(State, chord, key, scancode, is_repeat) if chord == 'C-p' and not App.mouse_down(1) then State.current_drawing_mode = 'freehand' elseif App.mouse_down(1) and chord == 'l' then diff --git a/edit.lua b/edit.lua index 97860b7..54b8aa9 100644 --- a/edit.lua +++ b/edit.lua @@ -56,11 +56,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height) 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,28 +119,12 @@ 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) +function edit.mouse_press(State, x,y, mouse_button, is_touch, presses) if State.search_term then return end State.mouse_down = mouse_button --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos)) @@ -190,7 +169,7 @@ function edit.mouse_press(State, x,y, mouse_button) State.selection1 = Text.final_loc_on_screen(State) end -function edit.mouse_release(State, x,y, mouse_button) +function edit.mouse_release(State, x,y, mouse_button, is_touch, presses) if State.search_term then return end --? print_and_log(('edit.mouse_release(%d,%d): cursor at %d,%d'):format(x,y, State.cursor1.line, State.cursor1.pos)) State.mouse_down = nil @@ -251,13 +230,10 @@ function edit.text_input(State, t) if State.search_term then State.search_term = State.search_term..t Text.search_next(State) - else - Text.text_input(State, t) end - schedule_save(State) end -function edit.keychord_press(State, chord, key) +function edit.keychord_press(State, chord, key, scancode, is_repeat) if State.selection1.line and -- printable character created using shift key => delete selection -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) @@ -309,29 +285,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 - 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) - Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks - schedule_save(State) - end - elseif chord == 'C-y' then - 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) - Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks - schedule_save(State) - end -- clipboard elseif chord == 'C-a' then State.selection1 = {line=1, pos=1} @@ -341,33 +294,8 @@ function edit.keychord_press(State, chord, key) if s then App.set_clipboard(s) end - elseif chord == 'C-x' then - local s = Text.cut_selection_and_record_undo_event(State) - if s then - App.set_clipboard(s) - end - schedule_save(State) - elseif chord == 'C-v' then - -- 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.get_clipboard() - 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 - record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - schedule_save(State) else - Text.keychord_press(State, chord) + Text.keychord_press(State, chord, key, scancode, is_repeat) end end diff --git a/file.lua b/file.lua index f7f832b..028ffb4 100644 --- a/file.lua +++ b/file.lua @@ -47,7 +47,7 @@ end function load_array(a) local result = {} local next_line = ipairs(a) - local i,line,drawing = 0, '' + local i,line = 0, '' while true do i,line = next_line(a, i) if i == nil then break end diff --git a/keychord.lua b/keychord.lua index f1b6a59..5de899d 100644 --- a/keychord.lua +++ b/keychord.lua @@ -2,13 +2,13 @@ Modifiers = {'lctrl', 'rctrl', 'lalt', 'ralt', 'lshift', 'rshift', 'lgui', 'rgui'} -function App.keypressed(key, scancode, isrepeat) +function App.keypressed(key, scancode, is_repeat) if array.find(Modifiers, key) then -- do nothing when the modifier is pressed return end -- include the modifier(s) when the non-modifer is pressed - App.keychord_press(App.combine_modifiers(key), key) + App.keychord_press(App.combine_modifiers(key), key, scancode, is_repeat) end function App.combine_modifiers(key) diff --git a/log.lua b/log.lua index 8903e08..4e428d8 100644 --- a/log.lua +++ b/log.lua @@ -1,38 +1,38 @@ function log(stack_frame_index, obj) - local info = debug.getinfo(stack_frame_index, 'Sl') - local msg - if type(obj) == 'string' then - msg = obj - else - msg = json.encode(obj) - end - love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n') + local info = debug.getinfo(stack_frame_index, 'Sl') + local msg + if type(obj) == 'string' then + msg = obj + else + msg = json.encode(obj) + end + love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n') end -- for section delimiters we'll use specific Unicode box characters function log_start(name, stack_frame_index) - if stack_frame_index == nil then - stack_frame_index = 3 - end - -- I'd like to use the unicode character \u{250c} here, but it doesn't work - -- in OpenBSD. - log(stack_frame_index, '[ u250c ' .. name) + if stack_frame_index == nil then + stack_frame_index = 3 + end + -- I'd like to use the unicode character \u{250c} here, but it doesn't work + -- in OpenBSD. + log(stack_frame_index, '[ u250c ' .. name) end function log_end(name, stack_frame_index) - if stack_frame_index == nil then - stack_frame_index = 3 - end - -- I'd like to use the unicode character \u{2518} here, but it doesn't work - -- in OpenBSD. - log(stack_frame_index, '] u2518 ' .. name) + if stack_frame_index == nil then + stack_frame_index = 3 + end + -- I'd like to use the unicode character \u{2518} here, but it doesn't work + -- in OpenBSD. + log(stack_frame_index, '] u2518 ' .. name) end function log_new(name, stack_frame_index) - if stack_frame_index == nil then - stack_frame_index = 4 - end - log_end(name, stack_frame_index) - log_start(name, stack_frame_index) + if stack_frame_index == nil then + stack_frame_index = 4 + end + log_end(name, stack_frame_index) + log_start(name, stack_frame_index) end -- rendering graphical objects within sections/boxes diff --git a/log_browser.lua b/log_browser.lua index 6e7e6db..fae9d6d 100644 --- a/log_browser.lua +++ b/log_browser.lua @@ -183,7 +183,7 @@ end function log_browser.quit(State) end -function log_browser.mouse_press(State, x,y, mouse_button) +function log_browser.mouse_press(State, x,y, mouse_button, is_touch, presses) local line_index = log_browser.line_index(State, x,y) if line_index == nil then -- below lower margin @@ -249,7 +249,7 @@ function log_browser.line_index(State, mx,my) end end -function log_browser.mouse_release(State, x,y, mouse_button) +function log_browser.mouse_release(State, x,y, mouse_button, is_touch, presses) end function log_browser.mouse_wheel_move(State, dx,dy) @@ -267,7 +267,7 @@ end function log_browser.text_input(State, t) end -function log_browser.keychord_press(State, chord, key) +function log_browser.keychord_press(State, chord, key, scancode, is_repeat) -- move if chord == 'up' then log_browser.up(State) diff --git a/main.lua b/main.lua index e7d35f4..83049c2 100644 --- a/main.lua +++ b/main.lua @@ -113,15 +113,15 @@ function check_love_version_for_tests() end end -function App.initialize(arg) +function App.initialize(arg, unfiltered_arg) love.keyboard.setKeyRepeat(true) love.graphics.setBackgroundColor(1,1,1) if Current_app == 'run' then - run.initialize(arg) + run.initialize(arg, unfiltered_arg) elseif Current_app == 'source' then - source.initialize(arg) + source.initialize(arg, unfiltered_arg) elseif current_app_is_warning() then else assert(false, 'unknown app "'..Current_app..'"') @@ -207,7 +207,7 @@ function App.update(dt) end end -function App.keychord_press(chord, key) +function App.keychord_press(chord, key, scancode, is_repeat) -- ignore events for some time after window in focus (mostly alt-tab) if Current_time < Last_focus_time + 0.01 then return @@ -251,9 +251,9 @@ function App.keychord_press(chord, key) return end if Current_app == 'run' then - if run.keychord_press then run.keychord_press(chord, key) end + if run.keychord_press then run.keychord_press(chord, key, scancode, is_repeat) end elseif Current_app == 'source' then - if source.keychord_press then source.keychord_press(chord, key) end + if source.keychord_press then source.keychord_press(chord, key, scancode, is_repeat) end else assert(false, 'unknown app "'..Current_app..'"') end @@ -293,24 +293,24 @@ function App.keyreleased(key, scancode) end end -function App.mousepressed(x,y, mouse_button) +function App.mousepressed(x,y, mouse_button, is_touch, presses) if current_app_is_warning() then return end --? print('mouse press', x,y) if Current_app == 'run' then - if run.mouse_press then run.mouse_press(x,y, mouse_button) end + if run.mouse_press then run.mouse_press(x,y, mouse_button, is_touch, presses) end elseif Current_app == 'source' then - if source.mouse_press then source.mouse_press(x,y, mouse_button) end + if source.mouse_press then source.mouse_press(x,y, mouse_button, is_touch, presses) end else assert(false, 'unknown app "'..Current_app..'"') end end -function App.mousereleased(x,y, mouse_button) +function App.mousereleased(x,y, mouse_button, is_touch, presses) if current_app_is_warning() then return end if Current_app == 'run' then - if run.mouse_release then run.mouse_release(x,y, mouse_button) end + if run.mouse_release then run.mouse_release(x,y, mouse_button, is_touch, presses) end elseif Current_app == 'source' then - if source.mouse_release then source.mouse_release(x,y, mouse_button) end + if source.mouse_release then source.mouse_release(x,y, mouse_button, is_touch, presses) end else assert(false, 'unknown app "'..Current_app..'"') end @@ -319,9 +319,9 @@ end function App.mousemoved(x,y, dx,dy, is_touch) if current_app_is_warning() then return end if Current_app == 'run' then - if run.mouse_move then run.mouse_move(dx,dy) end + if run.mouse_move then run.mouse_move(x,y, dx,dy, is_touch) end elseif Current_app == 'source' then - if source.mouse_move then source.mouse_move(dx,dy) end + if source.mouse_move then source.mouse_move(x,y, dx,dy, is_touch) end else assert(false, 'unknown app "'..Current_app..'"') end diff --git a/reference.md b/reference.md index 972ab1d..80309c3 100644 --- a/reference.md +++ b/reference.md @@ -203,9 +203,6 @@ early warning if you break something. `x=right`. Wraps long lines at word boundaries where possible, or in the middle of words (no hyphenation yet) when it must. -* `edit.quit()` -- calling this ensures any final edits are flushed to disk - before the app exits. - * `edit.draw(state)` -- call this from `App.draw` to display the current editor state on the app window as requested in the call to `edit.initialize_state` that created `state`. diff --git a/run.lua b/run.lua index 9a7051d..e317733 100644 --- a/run.lua +++ b/run.lua @@ -11,7 +11,7 @@ function run.initialize_globals() end -- called only for real run -function run.initialize(arg) +function run.initialize(arg, unfiltered_arg) log_new('run') if Settings then run.load_settings() @@ -34,7 +34,7 @@ function run.initialize(arg) -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('text.love - '..Editor_state.filename) + love.window.setTitle('view.love - '..Editor_state.filename) @@ -100,7 +100,7 @@ function run.initialize_window_geometry() App.screen.resize(App.screen.width, App.screen.height, App.screen.flags) end -function run.resize(w, h) +function run.resize(w,h) --? print(("Window resized to width: %d and height: %d."):format(w, h)) App.screen.width, App.screen.height = w, h Text.redraw_all(Editor_state) @@ -128,7 +128,7 @@ function run.file_drop(file) -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('text.love - '..Editor_state.filename) + love.window.setTitle('view.love - '..Editor_state.filename) @@ -166,15 +166,15 @@ function absolutize(path) return path end -function run.mouse_press(x,y, mouse_button) +function run.mouse_press(x,y, mouse_button, is_touch, presses) Cursor_time = 0 -- ensure cursor is visible immediately after it moves love.keyboard.setTextInput(true) -- bring up keyboard on touch screen - return edit.mouse_press(Editor_state, x,y, mouse_button) + return edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses) end -function run.mouse_release(x,y, mouse_button) +function run.mouse_release(x,y, mouse_button, is_touch, presses) Cursor_time = 0 -- ensure cursor is visible immediately after it moves - return edit.mouse_release(Editor_state, x,y, mouse_button) + return edit.mouse_release(Editor_state, x,y, mouse_button, is_touch, presses) end function run.mouse_wheel_move(dx,dy) @@ -187,9 +187,9 @@ function run.text_input(t) return edit.text_input(Editor_state, t) end -function run.keychord_press(chord, key) +function run.keychord_press(chord, key, scancode, is_repeat) Cursor_time = 0 -- ensure cursor is visible immediately after it moves - return edit.keychord_press(Editor_state, chord, key) + return edit.keychord_press(Editor_state, chord, key, scancode, is_repeat) end function run.key_release(key, scancode) 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 78affdc..018ddef 100644 --- a/select.lua +++ b/select.lua @@ -79,60 +79,6 @@ function Text.mouse_pos(State) return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1) end -function Text.cut_selection_and_record_undo_event(State) - if State.selection1.line == nil then return end - local result = Text.selection(State) - Text.delete_selection_and_record_undo_event(State) - return result -end - -function Text.delete_selection_and_record_undo_event(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, ('minl %d not < maxl %d'):format(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 0644195..a998b67 100644 --- a/source.lua +++ b/source.lua @@ -56,7 +56,7 @@ function source.initialize_globals() end -- called only for real run -function source.initialize() +function source.initialize(arg, unfiltered_arg) log_new('source') if Settings and Settings.source then source.load_settings() @@ -74,7 +74,7 @@ function source.initialize() -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('text.love - source - '..Editor_state.filename) + love.window.setTitle('view.love - source - '..Editor_state.filename) @@ -173,7 +173,7 @@ function source.initialize_window_geometry() App.screen.resize(App.screen.width, App.screen.height, App.screen.flags) end -function source.resize(w, h) +function source.resize(w,h) --? print(("Window resized to width: %d and height: %d."):format(w, h)) App.screen.width, App.screen.height = w, h Text.redraw_all(Editor_state) @@ -207,7 +207,7 @@ function source.file_drop(file) -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('text.love - source') + love.window.setTitle('view.love - source') @@ -283,7 +283,7 @@ function source.settings() } end -function source.mouse_press(x,y, mouse_button) +function source.mouse_press(x,y, mouse_button, is_touch, presses) Cursor_time = 0 -- ensure cursor is visible immediately after it moves love.keyboard.setTextInput(true) -- bring up keyboard on touch screen --? print('mouse click', x, y) @@ -291,7 +291,7 @@ function source.mouse_press(x,y, mouse_button) --? print(Log_browser_state.left, Log_browser_state.right) if Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then -- send click to buttons - edit.mouse_press(Editor_state, x,y, mouse_button) + edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses) return end if x < Editor_state.right + Margin_right then @@ -300,23 +300,23 @@ function source.mouse_press(x,y, mouse_button) Focus = 'edit' return end - edit.mouse_press(Editor_state, x,y, mouse_button) + edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses) elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then --? print('click on log_browser side') if Focus ~= 'log_browser' then Focus = 'log_browser' return end - log_browser.mouse_press(Log_browser_state, x,y, mouse_button) + log_browser.mouse_press(Log_browser_state, x,y, mouse_button, is_touch, presses) end end -function source.mouse_release(x,y, mouse_button) +function source.mouse_release(x,y, mouse_button, is_touch, presses) Cursor_time = 0 -- ensure cursor is visible immediately after it moves if Focus == 'edit' then - return edit.mouse_release(Editor_state, x,y, mouse_button) + return edit.mouse_release(Editor_state, x,y, mouse_button, is_touch, presses) else - return log_browser.mouse_release(Log_browser_state, x,y, mouse_button) + return log_browser.mouse_release(Log_browser_state, x,y, mouse_button, is_touch, presses) end end @@ -342,7 +342,7 @@ function source.text_input(t) end end -function source.keychord_press(chord, key) +function source.keychord_press(chord, key, scancode, is_repeat) Cursor_time = 0 -- ensure cursor is visible immediately after it moves --? print('source keychord') if Show_file_navigator then @@ -382,9 +382,9 @@ function source.keychord_press(chord, key) return end if Focus == 'edit' then - return edit.keychord_press(Editor_state, chord, key) + return edit.keychord_press(Editor_state, chord, key, scancode, is_repeat) else - return log_browser.keychord_press(Log_browser_state, chord, key) + return log_browser.keychord_press(Log_browser_state, chord, key, scancode, is_repeat) end end @@ -393,6 +393,6 @@ function source.key_release(key, scancode) if Focus == 'edit' then return edit.key_release(Editor_state, key, scancode) else - return log_browser.keychord_press(Log_browser_state, chordkey, scancode) + return log_browser.key_release(Log_browser_state, key, scancode) end end diff --git a/source_edit.lua b/source_edit.lua index a8dd682..77487ea 100644 --- a/source_edit.lua +++ b/source_edit.lua @@ -143,13 +143,13 @@ end function edit.put_cursor_on_next_text_line(State) local line = State.cursor1.line - while line < #State.lines do + if State.lines[line].mode == 'text' then return end + while line <= #State.lines and State.lines[line].mode ~= 'text' do line = line+1 - if State.lines[line].mode == 'text' then - State.cursor1.line = line - State.cursor1.pos = 1 - break - end + end + if line <= #State.lines and State.lines[line].mode == 'text' then + State.cursor1.line = line + State.cursor1.pos = 1 end end @@ -232,7 +232,7 @@ function edit.quit(State) end end -function edit.mouse_press(State, x,y, mouse_button) +function edit.mouse_press(State, x,y, mouse_button, is_touch, presses) if State.search_term then return end State.mouse_down = mouse_button --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos)) @@ -279,7 +279,7 @@ function edit.mouse_press(State, x,y, mouse_button) State.lines.current_drawing_index = line_index State.lines.current_drawing = line Drawing.before = snapshot(State, line_index) - Drawing.mouse_press(State, line_index, x,y, mouse_button) + Drawing.mouse_press(State, line_index, x,y, mouse_button, is_touch, presses) return end end @@ -292,12 +292,12 @@ function edit.mouse_press(State, x,y, mouse_button) State.selection1 = Text.final_text_loc_on_screen(State) end -function edit.mouse_release(State, x,y, mouse_button) +function edit.mouse_release(State, x,y, mouse_button, is_touch, presses) if State.search_term then return end --? print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos)) State.mouse_down = nil if State.lines.current_drawing then - Drawing.mouse_release(State, x,y, mouse_button) + Drawing.mouse_release(State, x,y, mouse_button, is_touch, presses) if Drawing.before then record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)}) Drawing.before = nil @@ -383,7 +383,7 @@ function edit.text_input(State, t) schedule_save(State) end -function edit.keychord_press(State, chord, key) +function edit.keychord_press(State, chord, key, scancode, is_repeat) if State.selection1.line and not State.lines.current_drawing and -- printable character created using shift key => delete selection @@ -502,7 +502,7 @@ function edit.keychord_press(State, chord, key) local drawing_index, drawing = Drawing.current_drawing(State) if drawing_index then local before = snapshot(State, drawing_index) - Drawing.keychord_press(State, chord) + Drawing.keychord_press(State, chord, key, scancode, is_repeat) record_undo_event(State, {before=before, after=snapshot(State, drawing_index)}) schedule_save(State) end @@ -535,7 +535,7 @@ function edit.keychord_press(State, chord, key) end schedule_save(State) else - Text.keychord_press(State, chord) + Text.keychord_press(State, chord, key, scancode, is_repeat) end end diff --git a/source_text.lua b/source_text.lua index 284ed00..c281acf 100644 --- a/source_text.lua +++ b/source_text.lua @@ -223,7 +223,7 @@ function Text.insert_at_cursor(State, t) end -- Don't handle any keys here that would trigger text_input above. -function Text.keychord_press(State, chord) +function Text.keychord_press(State, chord, key, scancode, is_repeat) --? print('chord', chord, State.selection1.line, State.selection1.pos) --== shortcuts that mutate text if chord == 'return' then diff --git a/source_text_tests.lua b/source_text_tests.lua index 7ed4caa..12fb9c6 100644 --- a/source_text_tests.lua +++ b/source_text_tests.lua @@ -319,7 +319,7 @@ function test_click_on_empty_line() check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits') end -function test_click_below_all_lines() +function test_click_below_final_line_of_file() -- display one line App.screen.init{width=50, height=80} Editor_state = edit.initialize_test_state() @@ -331,8 +331,9 @@ function test_click_below_all_lines() -- click below first line edit.draw(Editor_state) edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+50, 1) - -- cursor doesn't move - check_eq(Editor_state.cursor1.line, 1, 'cursor') + -- cursor goes to bottom + check_eq(Editor_state.cursor1.line, 1, 'cursor:line') + check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') -- selection remains empty check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits') end diff --git a/source_undo.lua b/source_undo.lua index d91fecd..772e5da 100644 --- a/source_undo.lua +++ b/source_undo.lua @@ -84,12 +84,12 @@ 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 {} + seen = seen or {} + if seen[obj] then return seen[obj] end local result = setmetatable({}, getmetatable(obj)) - s[obj] = result + seen[obj] = result for k,v in pairs(obj) do - result[deepcopy(k, s)] = deepcopy(v, s) + result[deepcopy(k, seen)] = deepcopy(v, seen) end return result end diff --git a/text.lua b/text.lua index da4d2a1..a417fab 100644 --- a/text.lua +++ b/text.lua @@ -38,12 +38,10 @@ function Text.draw(State, line_index, y, startpos) end else if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then - Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y) + Text.pretend_draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y) elseif pos + frag_len == State.cursor1.pos then - -- Show cursor at end of line. - -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing. - -- It seems useful to see a cursor whether your eye is on the left or right margin. - Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y) + -- Keep pretend cursor_x/cursor_y in sync with upstream. + Text.pretend_draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y) end end end @@ -80,6 +78,11 @@ function Text.draw_cursor(State, x, y) State.cursor_y = y+State.line_height end +function Text.pretend_draw_cursor(State, x, y) + State.cursor_x = x + State.cursor_y = y+State.line_height +end + function Text.populate_screen_line_starting_pos(State, line_index) local line = State.lines[line_index] local line_cache = State.line_cache[line_index] @@ -129,124 +132,13 @@ function Text.text_input(State, t) -- Key mutated by the keyboard layout. Continue below. end end - local before = snapshot(State, State.cursor1.line) ---? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.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) +function Text.keychord_press(State, chord, key, scancode, is_repeat) --? print('chord', chord, State.selection1.line, State.selection1.pos) - --== shortcuts that mutate text (must schedule_save) - if chord == 'return' then - local before_line = State.cursor1.line - local before = snapshot(State, before_line) - Text.insert_return(State) - if State.cursor_y > App.screen.height - State.line_height then - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) - end - record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - schedule_save(State) - 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) - 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) - end - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - schedule_save(State) - elseif chord == 'backspace' then - if State.selection1.line then - Text.delete_selection_and_record_undo_event(State) - 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 line caches to avoid memory leaks - end - Text.clear_screen_line_cache(State, State.cursor1.line) - assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - schedule_save(State) - elseif chord == 'delete' then - if State.selection1.line then - Text.delete_selection_and_record_undo_event(State) - 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) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - schedule_save(State) --== shortcuts that move the cursor - elseif chord == 'left' then + if chord == 'left' then Text.left(State) State.selection1 = {} elseif chord == 'right' then @@ -330,15 +222,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) State.screen_top1 = Text.previous_screen_top1(State) State.cursor1 = deepcopy(State.screen_top1) diff --git a/text_tests b/text_tests index 2a31131..85d9de9 100644 --- a/text_tests +++ b/text_tests @@ -23,7 +23,7 @@ click on wrapping line rendered from partway at top of screen click past end of wrapping line click past end of wrapping line containing non ascii click past end of word wrapping line -click below final line does nothing +click below final line of file # cursor movement move left diff --git a/text_tests.lua b/text_tests.lua index 7bf2221..79c38c5 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -15,43 +15,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', '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} - edit.run_after_keychord(Editor_state, 'C-m', 'm') -end - function test_move_left() App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() @@ -293,7 +256,7 @@ function test_click_on_empty_line() check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits') end -function test_click_below_all_lines() +function test_click_below_final_line_of_file() -- display one line App.screen.init{width=50, height=80} Editor_state = edit.initialize_test_state() @@ -305,8 +268,9 @@ function test_click_below_all_lines() -- click below first line edit.draw(Editor_state) edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+50, 1) - -- cursor doesn't move - check_eq(Editor_state.cursor1.line, 1, 'cursor') + -- cursor goes to bottom + check_eq(Editor_state.cursor1.line, 1, 'cursor:line') + check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') -- selection remains empty check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits') end @@ -574,43 +538,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} - 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} - 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} @@ -628,154 +555,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} - edit.draw(Editor_state) - -- press a key - edit.run_after_keychord(Editor_state, 'C-x', '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} - edit.draw(Editor_state) - -- set clipboard - App.clipboard = 'xyz' - -- paste selection - edit.run_after_keychord(Editor_state, 'C-v', '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} - 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', '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_move_cursor_using_mouse() -function test_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} - 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} - 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', '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} - -- hitting the enter key splits the line - edit.run_after_keychord(Editor_state, 'return', '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} - 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', '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.startpos for each line + 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() @@ -929,22 +723,6 @@ function test_select_all_text() check_eq(Editor_state.cursor1.pos, 8, '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.selection1 = {} - edit.draw(Editor_state) - -- try to cut without selecting text - edit.run_after_keychord(Editor_state, 'C-x', '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() @@ -1375,104 +1153,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} - 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', '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} - 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', '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} - 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} - 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} @@ -1587,293 +1267,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} - 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+2,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, 25, '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} - 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', '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} - 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', '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', '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', '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', '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', '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', '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', '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} - -- 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', '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} - -- delete a character - edit.run_after_keychord(Editor_state, 'backspace', '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', '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} - 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', 'z') - edit.run_after_keychord(Editor_state, 'C-z', '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 index e0cd730..4001fef 100644 --- a/undo.lua +++ b/undo.lua @@ -1,92 +1,12 @@ --- 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 - --- makes a copy of lines on every single keystroke; will be inefficient with really long lines. --- 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, 'failed to snapshot operation for undo history') - if e == nil then - e = s - end - assert(#State.lines > 0, 'failed to snapshot operation for undo history') - 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 - } - for i=s,e do - table.insert(event.lines, deepcopy(State.lines[i])) - 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, 'failed to patch undo operation') - 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, 'failed to patch undo operation') - for i=1,#to.lines do - table.insert(lines, to.start_line+i-1, to.lines[i]) - end -end - -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080 function deepcopy(obj, seen) if type(obj) ~= 'table' then return obj end - if seen and seen[obj] then return seen[obj] end - local s = seen or {} + seen = seen or {} + if seen[obj] then return seen[obj] end local result = setmetatable({}, getmetatable(obj)) - s[obj] = result + seen[obj] = result for k,v in pairs(obj) do - result[deepcopy(k, s)] = deepcopy(v, s) + result[deepcopy(k, seen)] = deepcopy(v, seen) end return result end |