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-- | 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-- | 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_undo.lua | 8 | ||||
-rw-r--r-- | text.lua | 137 | ||||
-rw-r--r-- | text_tests.lua | 622 | ||||
-rw-r--r-- | undo.lua | 88 |
19 files changed, 138 insertions, 1105 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/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/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_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.lua b/text_tests.lua index 962de7d..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() @@ -575,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} @@ -629,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() @@ -930,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() @@ -1376,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} @@ -1588,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 |