diff options
-rw-r--r-- | README.md | 14 | ||||
-rw-r--r-- | app.lua | 6 | ||||
-rw-r--r-- | conf.lua | 3 | ||||
-rw-r--r-- | drawing.lua | 6 | ||||
-rw-r--r-- | edit.lua | 50 | ||||
-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 | 29 | ||||
-rw-r--r-- | reference.md | 3 | ||||
-rw-r--r-- | run.lua | 17 | ||||
-rw-r--r-- | search.lua | 2 | ||||
-rw-r--r-- | select.lua | 14 | ||||
-rw-r--r-- | source.lua | 27 | ||||
-rw-r--r-- | source_edit.lua | 66 | ||||
-rw-r--r-- | source_select.lua | 6 | ||||
-rw-r--r-- | source_text.lua | 63 | ||||
-rw-r--r-- | source_text_tests.lua | 35 | ||||
-rw-r--r-- | source_undo.lua | 29 | ||||
-rw-r--r-- | text.lua | 71 | ||||
-rw-r--r-- | text_tests | 2 | ||||
-rw-r--r-- | text_tests.lua | 35 | ||||
-rw-r--r-- | undo.lua | 26 |
24 files changed, 292 insertions, 274 deletions
diff --git a/README.md b/README.md index 27ce06d..0887f4a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # An editor 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. @@ -10,8 +12,7 @@ modifications break something. ## Getting started Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and -extremely well-behaved. I'll assume below that you can invoke it using the -`love` command, but that might vary depending on your OS. +extremely well-behaved. To run from the terminal, [pass this directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games), optionally with a file path to edit. @@ -47,12 +48,6 @@ found anything amiss: http://akkartik.name/contact * No support yet for right-to-left languages. -* Undo/redo may be sluggish in large files. Large files may grow sluggish in - other ways. Works well in all circumstances with files under 50KB. - -* If you kill the process, say by force-quitting because things things get - sluggish, you can lose data. - * Can't scroll while selecting text with mouse. * No scrollbars yet. That stuff is hard. @@ -90,4 +85,5 @@ Further forks are encouraged. If you show me your fork, I'll link to it here. ## Feedback -[Most appreciated.](http://akkartik.name/contact) +[Most appreciated.](http://akkartik.name/contact) Messages, PRs, patches, +forks, it's all good. diff --git a/app.lua b/app.lua index 263518c..2375ff9 100644 --- a/app.lua +++ b/app.lua @@ -131,7 +131,7 @@ function App.run_tests() end table.sort(sorted_names) --? App.initialize_for_test() -- debug: run a single test at a time like these 2 lines ---? test_click_below_all_lines() +--? test_search() for _,name in ipairs(sorted_names) do App.initialize_for_test() --? print('=== '..name) @@ -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..d9878da --- /dev/null +++ b/conf.lua @@ -0,0 +1,3 @@ +function love.conf(t) + t.identity = 'text' +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 eaa415e..edfcb20 100644 --- a/edit.lua +++ b/edit.lua @@ -1,7 +1,7 @@ -- some constants people might like to tweak Text_color = {r=0, g=0, b=0} Cursor_color = {r=1, g=0, b=0} -Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text +Highlight_color = {r=0.7, g=0.7, b=0.9, a=0.4} -- selected text Margin_top = 15 Margin_left = 25 @@ -97,7 +97,6 @@ function edit.invalid_cursor1(State) return cursor1.pos > #State.lines[cursor1.line].data + 1 end --- return y drawn until function edit.draw(State) love.graphics.setFont(State.font) App.color(Text_color) @@ -122,7 +121,6 @@ function edit.draw(State) if State.search_term then Text.draw_search_bar(State) end - return y end function edit.update(State, dt) @@ -147,8 +145,7 @@ function edit.quit(State) end end -function edit.mouse_press(State, x,y, mouse_button) - love.keyboard.setTextInput(true) -- bring up keyboard on touch screen +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,15 +187,15 @@ function edit.mouse_press(State, x,y, mouse_button) State.old_cursor1 = State.cursor1 State.old_selection1 = State.selection1 State.mousepress_shift = App.shift_down() - State.selection1 = Text.final_text_loc_on_screen(State) + 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 if y < State.top then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) edit.clean_up_mouse_press(State) return end @@ -216,7 +213,7 @@ function edit.mouse_release(State, x,y, mouse_button) end -- still here? mouse release is below all screen lines - State.cursor1 = Text.final_text_loc_on_screen(State) + State.cursor1 = Text.final_loc_on_screen(State) edit.clean_up_mouse_press(State) --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos)) end @@ -237,7 +234,7 @@ end function edit.mouse_wheel_move(State, dx,dy) if dy > 0 then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) for i=1,math.floor(dy) do Text.up(State) end @@ -260,13 +257,13 @@ 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 -- printable character created using shift key => delete selection -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) (not App.shift_down() or utf8.len(key) == 1) and chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(key) then - Text.delete_selection(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) end if State.search_term then if chord == 'escape' then @@ -274,7 +271,7 @@ function edit.keychord_press(State, chord, key) State.cursor1 = State.search_backup.cursor State.screen_top1 = State.search_backup.screen_top State.search_backup = nil - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks elseif chord == 'return' then State.search_term = nil State.search_backup = nil @@ -282,9 +279,14 @@ function edit.keychord_press(State, chord, key) local len = utf8.len(State.search_term) local byte_offset = Text.offset(State.search_term, len) State.search_term = string.sub(State.search_term, 1, byte_offset-1) - elseif chord == 'down' then - State.cursor1.pos = State.cursor1.pos+1 + State.cursor = deepcopy(State.search_backup.cursor) + State.screen_top = deepcopy(State.search_backup.screen_top) Text.search_next(State) + elseif chord == 'down' then + if #State.search_term > 0 then + Text.right(State) + Text.search_next(State) + end elseif chord == 'up' then Text.search_previous(State) end @@ -316,9 +318,7 @@ function edit.keychord_press(State, chord, key) State.cursor1 = deepcopy(src.cursor) State.selection1 = deepcopy(src.selection) patch(State.lines, event.after, event.before) - patch_placeholders(State.line_cache, event.after, event.before) - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) + 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 @@ -329,8 +329,7 @@ function edit.keychord_press(State, chord, key) State.cursor1 = deepcopy(src.cursor) State.selection1 = deepcopy(src.selection) patch(State.lines, event.before, event.after) - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks schedule_save(State) end -- clipboard @@ -343,7 +342,7 @@ function edit.keychord_press(State, chord, key) App.set_clipboard(s) end elseif chord == 'C-x' then - local s = Text.cut_selection(State, State.left, State.right) + local s = Text.cut_selection_and_record_undo_event(State) if s then App.set_clipboard(s) end @@ -365,20 +364,19 @@ function edit.keychord_press(State, chord, key) if Text.cursor_out_of_screen(State) then Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - -- dispatch to text + schedule_save(State) else - Text.keychord_press(State, chord) + Text.keychord_press(State, chord, key, scancode, is_repeat) end end function edit.key_release(State, key, scancode) end -function edit.update_font_settings(State, font_height) +function edit.update_font_settings(State, font_height, font) State.font_height = font_height - State.font = love.graphics.newFont(State.font_height) + State.font = font or love.graphics.newFont(State.font_height) State.line_height = math.floor(font_height*1.3) 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 582f05a..83049c2 100644 --- a/main.lua +++ b/main.lua @@ -113,16 +113,15 @@ function check_love_version_for_tests() end end -function App.initialize(arg) - love.keyboard.setTextInput(true) -- bring up keyboard on touch screen +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..'"') @@ -208,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 @@ -252,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 @@ -294,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 @@ -320,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 307505d..9511726 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() @@ -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) @@ -166,14 +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 - return edit.mouse_press(Editor_state, x,y, mouse_button) + love.keyboard.setTextInput(true) -- bring up keyboard on touch screen + 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) @@ -186,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/search.lua b/search.lua index d3a5fea..4aa1f02 100644 --- a/search.lua +++ b/search.lua @@ -17,6 +17,7 @@ function Text.draw_search_bar(State) end function Text.search_next(State) + if #State.search_term == 0 then return end -- search current line from cursor local curr_pos = State.cursor1.pos local curr_line = State.lines[State.cursor1.line].data @@ -71,6 +72,7 @@ function Text.search_next(State) end function Text.search_previous(State) + if #State.search_term == 0 then return end -- search current line before cursor local curr_pos = State.cursor1.pos local curr_line = State.lines[State.cursor1.line].data diff --git a/select.lua b/select.lua index e8df6f9..78affdc 100644 --- a/select.lua +++ b/select.lua @@ -1,9 +1,8 @@ -- helpers for selecting portions of text --- Return any intersection of the region from State.selection1 to State.cursor1 (or --- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and --- currently over a drawing) with the region between {line=line_index, pos=apos} --- and {line=line_index, pos=bpos}. +-- Return any intersection of the region from State.selection1 to +-- State.cursor1 (or current mouse, if mouse is pressed) with the region +-- between {line=line_index, pos=apos} and {line=line_index, pos=bpos}. -- apos must be less than bpos. However State.selection1 and State.cursor1 can be in any order. -- Result: positions spos,epos between apos,bpos. function Text.clip_selection(State, line_index, apos, bpos) @@ -45,7 +44,6 @@ function Text.clip_selection(State, line_index, apos, bpos) end -- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line --- Creates text objects every time, so use this sparingly. -- Returns some intermediate computation useful elsewhere. function Text.draw_highlight(State, line, x,y, pos, lo,hi) if lo then @@ -81,14 +79,14 @@ function Text.mouse_pos(State) return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1) end -function Text.cut_selection(State) +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(State) + Text.delete_selection_and_record_undo_event(State) return result end -function Text.delete_selection(State) +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) diff --git a/source.lua b/source.lua index c85517d..bb42440 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() @@ -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) @@ -283,14 +283,15 @@ 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) --? print(Editor_state.left, Editor_state.right) --? 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 @@ -299,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 @@ -341,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 @@ -381,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 @@ -392,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 5351857..77487ea 100644 --- a/source_edit.lua +++ b/source_edit.lua @@ -53,8 +53,7 @@ function edit.initialize_state(top, left, right, font, font_height, line_height) -- rendering wrapped text lines needs some additional short-lived data per line: -- startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen - -- fragments: snippets of the line guaranteed to not straddle screen lines - -- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line + -- screen_line_starting_pos: optional array of codepoint indices if it wraps over more than one screen line line_cache = {}, -- Given wrapping, any potential location for the text cursor can be described in two ways: @@ -143,14 +142,13 @@ function edit.cursor_on_text(State) end function edit.put_cursor_on_next_text_line(State) - while true do - if State.cursor1.line >= #State.lines then - break - end - if State.lines[State.cursor1.line].mode == 'text' then - break - end - State.cursor1.line = State.cursor1.line+1 + local line = State.cursor1.line + if State.lines[line].mode == 'text' then return end + while line <= #State.lines and State.lines[line].mode ~= 'text' do + line = line+1 + end + if line <= #State.lines and State.lines[line].mode == 'text' then + State.cursor1.line = line State.cursor1.pos = 1 end end @@ -190,8 +188,9 @@ function edit.draw(State, hide_cursor, show_line_numbers) if State.cursor1.line >= line_index then State.cursor1.line = State.cursor1.line+1 end - schedule_save(State) record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)}) + Drawing.before = nil + schedule_save(State) end, }) end @@ -233,8 +232,7 @@ function edit.quit(State) end end -function edit.mouse_press(State, x,y, mouse_button) - love.keyboard.setTextInput(true) -- bring up keyboard on touch screen +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)) @@ -281,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 @@ -294,21 +292,21 @@ 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) - schedule_save(State) + 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 end + schedule_save(State) else --? print_and_log('edit.mouse_release: no current drawing') if y < State.top then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) edit.clean_up_mouse_press(State) return end @@ -351,7 +349,7 @@ end function edit.mouse_wheel_move(State, dx,dy) if dy > 0 then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) edit.put_cursor_on_next_text_line(State) for i=1,math.floor(dy) do Text.up(State) @@ -385,14 +383,14 @@ 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 -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) (not App.shift_down() or utf8.len(key) == 1) and chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(key) then - Text.delete_selection(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) end if State.search_term then if chord == 'escape' then @@ -400,7 +398,7 @@ function edit.keychord_press(State, chord, key) State.cursor1 = State.search_backup.cursor State.screen_top1 = State.search_backup.screen_top State.search_backup = nil - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks elseif chord == 'return' then State.search_term = nil State.search_backup = nil @@ -408,9 +406,14 @@ function edit.keychord_press(State, chord, key) local len = utf8.len(State.search_term) local byte_offset = Text.offset(State.search_term, len) State.search_term = string.sub(State.search_term, 1, byte_offset-1) - elseif chord == 'down' then - State.cursor1.pos = State.cursor1.pos+1 + State.cursor = deepcopy(State.search_backup.cursor) + State.screen_top = deepcopy(State.search_backup.screen_top) Text.search_next(State) + elseif chord == 'down' then + if #State.search_term > 0 then + Text.right(State) + Text.search_next(State) + end elseif chord == 'up' then Text.search_previous(State) end @@ -442,11 +445,9 @@ function edit.keychord_press(State, chord, key) State.cursor1 = deepcopy(src.cursor) State.selection1 = deepcopy(src.selection) patch(State.lines, event.after, event.before) - patch_placeholders(State.line_cache, event.after, event.before) -- invalidate various cached bits of lines State.lines.current_drawing = nil - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) + 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 @@ -459,8 +460,7 @@ function edit.keychord_press(State, chord, key) patch(State.lines, event.before, event.after) -- invalidate various cached bits of lines State.lines.current_drawing = nil - -- if we're scrolling, reclaim all fragments to avoid memory leaks - Text.redraw_all(State) + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks schedule_save(State) end -- clipboard @@ -473,7 +473,7 @@ function edit.keychord_press(State, chord, key) App.set_clipboard(s) end elseif chord == 'C-x' then - local s = Text.cut_selection(State, State.left, State.right) + local s = Text.cut_selection_and_record_undo_event(State) if s then App.set_clipboard(s) end @@ -495,14 +495,14 @@ function edit.keychord_press(State, chord, key) if Text.cursor_out_of_screen(State) then Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) + schedule_save(State) -- dispatch to drawing or text elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then 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_select.lua b/source_select.lua index b67dd16..a223b80 100644 --- a/source_select.lua +++ b/source_select.lua @@ -83,14 +83,14 @@ function Text.mouse_pos(State) return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1) end -function Text.cut_selection(State) +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(State) + Text.delete_selection_and_record_undo_event(State) return result end -function Text.delete_selection(State) +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) diff --git a/source_text.lua b/source_text.lua index 6e0c4f9..c281acf 100644 --- a/source_text.lua +++ b/source_text.lua @@ -90,9 +90,9 @@ function Text.screen_line(line, line_cache, i) if i >= #line_cache.screen_line_starting_pos then return line.data:sub(offset) end - local endpos = line_cache.screen_line_starting_pos[i+1]-1 + local endpos = line_cache.screen_line_starting_pos[i+1] local end_offset = Text.offset(line.data, endpos) - return line.data:sub(offset, end_offset) + return line.data:sub(offset, end_offset-1) end function Text.draw_cursor(State, x, y) @@ -198,7 +198,7 @@ function Text.text_input(State, t) if App.mouse_down(1) then return end if App.any_modifier_down() then if App.key_down(t) then - -- The modifiers didn't change the key. Handle it in keychord_pressed. + -- The modifiers didn't change the key. Handle it in keychord_press. return else -- Key mutated by the keyboard layout. Continue below. @@ -223,19 +223,18 @@ 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 local before_line = State.cursor1.line local before = snapshot(State, before_line) Text.insert_return(State) - State.selection1 = {} if State.cursor_y > App.screen.height - State.line_height then Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) + 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) @@ -245,11 +244,11 @@ function Text.keychord_press(State, chord) 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 - schedule_save(State) 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(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) schedule_save(State) return end @@ -289,15 +288,15 @@ function Text.keychord_press(State, chord) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + 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)) - schedule_save(State) 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(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) schedule_save(State) return end @@ -327,8 +326,8 @@ function Text.keychord_press(State, chord) table.remove(State.line_cache, State.cursor1.line+1) end Text.clear_screen_line_cache(State, State.cursor1.line) - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) + schedule_save(State) --== shortcuts that move the cursor elseif chord == 'left' then Text.left(State) @@ -338,12 +337,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-left' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.left(State) elseif chord == 'S-right' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.right(State) -- C- hotkeys reserved for drawings, so we'll use M- @@ -355,12 +354,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'M-S-left' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.word_left(State) elseif chord == 'M-S-right' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.word_right(State) elseif chord == 'home' then @@ -371,12 +370,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-home' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.start_of_line(State) elseif chord == 'S-end' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.end_of_line(State) elseif chord == 'up' then @@ -387,12 +386,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-up' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.up(State) elseif chord == 'S-down' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.down(State) elseif chord == 'pageup' then @@ -403,12 +402,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-pageup' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.pageup(State) elseif chord == 'S-pagedown' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.pagedown(State) end @@ -425,9 +424,9 @@ end function Text.pageup(State) State.screen_top1 = Text.previous_screen_top1(State) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end -- return the top y coordinate of a given line_index, @@ -474,9 +473,9 @@ end function Text.pagedown(State) State.screen_top1 = Text.screen_bottom1(State) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end -- return the location of the start of the bottom-most line on screen @@ -538,7 +537,7 @@ function Text.up(State) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end end @@ -594,7 +593,7 @@ end function Text.start_of_line(State) State.cursor1.pos = 1 if Text.lt1(State.cursor1, State.screen_top1) then - State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy + State.screen_top1 = deepcopy(State.cursor1) end end @@ -684,7 +683,7 @@ function Text.left(State) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end end @@ -829,7 +828,7 @@ function Text.snap_cursor_to_bottom_of_screen(State) State.screen_top1 = Text.to1(State, top2) --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos) --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end function Text.in_line(State, line_index, x,y) @@ -1103,7 +1102,7 @@ function Text.tweak_screen_top_and_cursor(State) -- make sure cursor is on screen local screen_bottom1 = Text.screen_bottom1(State) if Text.lt1(State.cursor1, State.screen_top1) then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) elseif State.cursor1.line >= screen_bottom1.line then if Text.cursor_out_of_screen(State) then State.cursor1 = Text.final_text_loc_on_screen(State) @@ -1118,7 +1117,7 @@ function Text.cursor_out_of_screen(State) end function Text.redraw_all(State) ---? print('clearing fragments') +--? print('clearing line caches') -- Perform some early sanity checking here, in hopes that we correctly call -- this whenever we change editor state. if State.right <= State.left then diff --git a/source_text_tests.lua b/source_text_tests.lua index 11cc823..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 @@ -2073,3 +2074,31 @@ function test_search_wrap_upwards() check_eq(Editor_state.cursor1.line, 1, '1/cursor:line') check_eq(Editor_state.cursor1.pos, 6, '1/cursor:pos') end + +function test_search_downwards_from_end_of_line() + App.screen.init{width=120, height=60} + Editor_state = edit.initialize_test_state() + Editor_state.lines = load_array{'abc', 'def', 'ghi'} + Text.redraw_all(Editor_state) + Editor_state.cursor1 = {line=1, pos=4} + Editor_state.screen_top1 = {line=1, pos=1} + edit.draw(Editor_state) + -- search for empty string + edit.run_after_keychord(Editor_state, 'C-f', 'f') + edit.run_after_keychord(Editor_state, 'down', 'down') + -- no crash +end + +function test_search_downwards_from_final_pos_of_line() + App.screen.init{width=120, height=60} + Editor_state = edit.initialize_test_state() + Editor_state.lines = load_array{'abc', 'def', 'ghi'} + Text.redraw_all(Editor_state) + Editor_state.cursor1 = {line=1, pos=3} + Editor_state.screen_top1 = {line=1, pos=1} + edit.draw(Editor_state) + -- search for empty string + edit.run_after_keychord(Editor_state, 'C-f', 'f') + edit.run_after_keychord(Editor_state, 'down', 'down') + -- no crash +end diff --git a/source_undo.lua b/source_undo.lua index e5dea93..772e5da 100644 --- a/source_undo.lua +++ b/source_undo.lua @@ -57,16 +57,8 @@ function snapshot(State, s,e) end_line=e, -- no filename; undo history is cleared when filename changes } - -- deep copy lines without cached stuff like text fragments for i=s,e do - local line = State.lines[i] - if line.mode == 'text' then - table.insert(event.lines, {mode='text', data=line.data}) -- I've forgotten: should we deepcopy(line.data)? - elseif line.mode == 'drawing' then - table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}}) - else - assert(false, ('unknown line mode %s'):format(line.mode)) - end + table.insert(event.lines, deepcopy(State.lines[i])) end return event end @@ -89,26 +81,15 @@ function patch(lines, from, to) end end -function patch_placeholders(line_cache, from, to) - assert(from.start_line == to.start_line, 'failed to patch undo operation') - for i=from.end_line,from.start_line,-1 do - table.remove(line_cache, i) - end - assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation') - for i=1,#to.lines do - table.insert(line_cache, to.start_line+i-1, {}) - end -end - -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080 function deepcopy(obj, seen) if type(obj) ~= 'table' then return obj end - if seen and seen[obj] then return seen[obj] end - local s = seen or {} + 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 5814693..9db8b16 100644 --- a/text.lua +++ b/text.lua @@ -47,7 +47,7 @@ function Text.draw(State, line_index, y, startpos) end end end - -- render fragment + -- render screen line App.color(Text_color) App.screen.print(screen_line, State.left,y) y = y + State.line_height @@ -65,9 +65,9 @@ function Text.screen_line(line, line_cache, i) if i >= #line_cache.screen_line_starting_pos then return line.data:sub(offset) end - local endpos = line_cache.screen_line_starting_pos[i+1]-1 + local endpos = line_cache.screen_line_starting_pos[i+1] local end_offset = Text.offset(line.data, endpos) - return line.data:sub(offset, end_offset) + return line.data:sub(offset, end_offset-1) end function Text.draw_cursor(State, x, y) @@ -123,7 +123,7 @@ function Text.text_input(State, t) if App.mouse_down(1) then return end if App.any_modifier_down() then if App.key_down(t) then - -- The modifiers didn't change the key. Handle it in keychord_pressed. + -- The modifiers didn't change the key. Handle it in keychord_press. return else -- Key mutated by the keyboard layout. Continue below. @@ -147,19 +147,18 @@ 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 + --== 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) - State.selection1 = {} if State.cursor_y > App.screen.height - State.line_height then Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) end - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) + 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) @@ -169,11 +168,11 @@ function Text.keychord_press(State, chord) 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 - schedule_save(State) 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(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) schedule_save(State) return end @@ -208,15 +207,15 @@ function Text.keychord_press(State, chord) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + 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)) - schedule_save(State) 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(State, State.left, State.right) + Text.delete_selection_and_record_undo_event(State) schedule_save(State) return end @@ -244,8 +243,8 @@ function Text.keychord_press(State, chord) table.remove(State.line_cache, State.cursor1.line+1) end Text.clear_screen_line_cache(State, State.cursor1.line) - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) + schedule_save(State) --== shortcuts that move the cursor elseif chord == 'left' then Text.left(State) @@ -255,12 +254,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-left' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.left(State) elseif chord == 'S-right' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.right(State) -- C- hotkeys reserved for drawings, so we'll use M- @@ -272,12 +271,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'M-S-left' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.word_left(State) elseif chord == 'M-S-right' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.word_right(State) elseif chord == 'home' then @@ -288,12 +287,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-home' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.start_of_line(State) elseif chord == 'S-end' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.end_of_line(State) elseif chord == 'up' then @@ -304,12 +303,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-up' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.up(State) elseif chord == 'S-down' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.down(State) elseif chord == 'pageup' then @@ -320,12 +319,12 @@ function Text.keychord_press(State, chord) State.selection1 = {} elseif chord == 'S-pageup' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.pageup(State) elseif chord == 'S-pagedown' then if State.selection1.line == nil then - State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} + State.selection1 = deepcopy(State.cursor1) end Text.pagedown(State) end @@ -342,9 +341,9 @@ end function Text.pageup(State) State.screen_top1 = Text.previous_screen_top1(State) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end -- return the top y coordinate of a given line_index, @@ -380,9 +379,9 @@ end function Text.pagedown(State) State.screen_top1 = Text.screen_bottom1(State) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end -- return the location of the start of the bottom-most line on screen @@ -435,7 +434,7 @@ function Text.up(State) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end end @@ -484,7 +483,7 @@ end function Text.start_of_line(State) State.cursor1.pos = 1 if Text.lt1(State.cursor1, State.screen_top1) then - State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy + State.screen_top1 = deepcopy(State.cursor1) end end @@ -564,7 +563,7 @@ function Text.left(State) line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end end @@ -611,7 +610,7 @@ function Text.pos_at_end_of_screen_line(State, loc1) assert(false, ('invalid pos %d'):format(loc1.pos)) end -function Text.final_text_loc_on_screen(State) +function Text.final_loc_on_screen(State) local screen_bottom1 = Text.screen_bottom1(State) return { line=screen_bottom1.line, @@ -658,7 +657,7 @@ function Text.snap_cursor_to_bottom_of_screen(State) State.screen_top1 = Text.to1(State, top2) --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos) --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos) - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end function Text.in_line(State, line_index, x,y) @@ -924,10 +923,10 @@ function Text.tweak_screen_top_and_cursor(State) -- make sure cursor is on screen local screen_bottom1 = Text.screen_bottom1(State) if Text.lt1(State.cursor1, State.screen_top1) then - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} + State.cursor1 = deepcopy(State.screen_top1) elseif State.cursor1.line >= screen_bottom1.line then if Text.cursor_out_of_screen(State) then - State.cursor1 = Text.final_text_loc_on_screen(State) + State.cursor1 = Text.final_loc_on_screen(State) end end end @@ -939,7 +938,7 @@ function Text.cursor_out_of_screen(State) end function Text.redraw_all(State) ---? print('clearing fragments') +--? print('clearing line caches') -- Perform some early sanity checking here, in hopes that we correctly call -- this whenever we change editor state. if State.right <= State.left then 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 9b3b60c..962de7d 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -293,7 +293,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 +305,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 @@ -1950,3 +1951,31 @@ function test_search_wrap_upwards() check_eq(Editor_state.cursor1.line, 1, '1/cursor:line') check_eq(Editor_state.cursor1.pos, 6, '1/cursor:pos') end + +function test_search_downwards_from_end_of_line() + App.screen.init{width=120, height=60} + Editor_state = edit.initialize_test_state() + Editor_state.lines = load_array{'abc', 'def', 'ghi'} + Text.redraw_all(Editor_state) + Editor_state.cursor1 = {line=1, pos=4} + Editor_state.screen_top1 = {line=1, pos=1} + edit.draw(Editor_state) + -- search for empty string + edit.run_after_keychord(Editor_state, 'C-f', 'f') + edit.run_after_keychord(Editor_state, 'down', 'down') + -- no crash +end + +function test_search_downwards_from_final_pos_of_line() + App.screen.init{width=120, height=60} + Editor_state = edit.initialize_test_state() + Editor_state.lines = load_array{'abc', 'def', 'ghi'} + Text.redraw_all(Editor_state) + Editor_state.cursor1 = {line=1, pos=3} + Editor_state.screen_top1 = {line=1, pos=1} + edit.draw(Editor_state) + -- search for empty string + edit.run_after_keychord(Editor_state, 'C-f', 'f') + edit.run_after_keychord(Editor_state, 'down', 'down') + -- no crash +end diff --git a/undo.lua b/undo.lua index a41ba38..69f7c31 100644 --- a/undo.lua +++ b/undo.lua @@ -1,8 +1,7 @@ -- undo/redo by managing the sequence of events in the current session -- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu --- Incredibly inefficient; we make a copy of lines on every single keystroke. --- The hope here is that we're either editing small files or just reading large files. +-- 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 @@ -55,10 +54,8 @@ function snapshot(State, s,e) end_line=e, -- no filename; undo history is cleared when filename changes } - -- deep copy lines without cached stuff like text fragments for i=s,e do - local line = State.lines[i] - table.insert(event.lines, {data=line.data}) -- I've forgotten: should we deepcopy(line.data)? + table.insert(event.lines, deepcopy(State.lines[i])) end return event end @@ -81,26 +78,15 @@ function patch(lines, from, to) end end -function patch_placeholders(line_cache, from, to) - assert(from.start_line == to.start_line, 'failed to patch undo operation') - for i=from.end_line,from.start_line,-1 do - table.remove(line_cache, i) - end - assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation') - for i=1,#to.lines do - table.insert(line_cache, to.start_line+i-1, {}) - end -end - -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080 function deepcopy(obj, seen) if type(obj) ~= 'table' then return obj end - if seen and seen[obj] then return seen[obj] end - local s = seen or {} + 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 |