diff options
-rw-r--r-- | Manual_tests.md | 2 | ||||
-rw-r--r-- | README.md | 121 | ||||
-rw-r--r-- | app.lua | 6 | ||||
-rw-r--r-- | conf.lua | 3 | ||||
-rw-r--r-- | drawing.lua | 6 | ||||
-rw-r--r-- | edit.lua | 292 | ||||
-rw-r--r-- | file.lua | 138 | ||||
-rw-r--r-- | keychord.lua | 4 | ||||
-rw-r--r-- | log.lua | 50 | ||||
-rw-r--r-- | log_browser.lua | 6 | ||||
-rw-r--r-- | main.lua | 41 | ||||
-rw-r--r-- | reference.md | 20 | ||||
-rw-r--r-- | run.lua | 21 | ||||
-rw-r--r-- | search.lua | 2 | ||||
-rw-r--r-- | select.lua | 16 | ||||
-rw-r--r-- | source.lua | 31 | ||||
-rw-r--r-- | source_edit.lua | 62 | ||||
-rw-r--r-- | source_select.lua | 6 | ||||
-rw-r--r-- | source_text.lua | 35 | ||||
-rw-r--r-- | source_text_tests.lua | 35 | ||||
-rw-r--r-- | source_undo.lua | 29 | ||||
-rw-r--r-- | text.lua | 236 | ||||
-rw-r--r-- | text_tests | 2 | ||||
-rw-r--r-- | text_tests.lua | 160 | ||||
-rw-r--r-- | undo.lua | 34 |
25 files changed, 402 insertions, 956 deletions
diff --git a/Manual_tests.md b/Manual_tests.md index 650bc76..3ee22c7 100644 --- a/Manual_tests.md +++ b/Manual_tests.md @@ -12,7 +12,7 @@ Initializing settings: - run with a filename on commandline, scroll around, quit; restart with new filename; window opens new filename with cursor up top - run editor, scroll around, move cursor to end of some line, quit; restart with new filename; window opens running the text editor in same position+dimensions - quit while running the text editor, restart; window opens running the text editor in same position+dimensions - - quit while editing source (color; no drawings; no selection), restart; window opens editing source in same position+dimensions + - quit while editing source (color; no selection), restart; window opens editing source in same position+dimensions - start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions - start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions - no log file; switching to source works diff --git a/README.md b/README.md index 690d94c..0887f4a 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,31 @@ -# Plain text with lines +# 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. -An editor for plain text where you can also seamlessly insert line drawings. Designed above all to be easy to modify and give you early warning if your modifications break something. -http://akkartik.name/lines.html - ## 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. Alternatively, turn it into a .love file you can double-click on: ``` -$ zip -r /tmp/lines.love *.lua +$ zip -r /tmp/text.love *.lua ``` -By default, lines.love reads/writes the file `lines.txt` in +By default, it reads/writes the file `lines.txt` in [a directory relative to this app](https://love2d.org/wiki/love.filesystem.getSourceBaseDirectory). -To open a different file, drop it on the lines.love window. +To open a different file, drop it on the app window. ## Keyboard shortcuts @@ -36,12 +38,7 @@ While editing text: * mouse drag or `shift` + movement to select text, `ctrl+a` to select all * `ctrl+e` to modify the sources -For shortcuts while editing drawings, consult the online help. Either: -* hover on a drawing and hit `ctrl+h`, or -* click on a drawing to start a stroke and then press and hold `h` to see your - options at any point during a stroke. - -lines.love has been exclusively tested so far with a US keyboard layout. If +Exclusively tested so far with a US keyboard layout. If you use a different layout, please let me know if things worked, or if you found anything amiss: http://akkartik.name/contact @@ -51,76 +48,42 @@ 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. lines.love 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. - -* The text cursor will always stay on the screen. This can have some strange - implications: - - * A long series of drawings will get silently skipped when you hit - page-down, until a line of text can be showed on screen. - * If there's no line of text at the top of the file, you may not be able - to scroll back up to the top with page-up. - - So far this app isn't really designed for drawing-heavy files. For now I'm - targeting mostly-text files with a few drawings mixed in. - -* No clipping yet for drawings. In particular, circles/squares/rectangles and - point labels can overflow a drawing. - -* If you ever see a crash when clicking on the mouse, it might be because a - mouse press and release need to happen in separate frames. Try pressing and - releasing more slowly and let me know if that helps or not. This is klunky, - sorry. - -* Touchpads can drag the mouse pointer using a light touch or a heavy click. - On Linux, drags using the light touch get interrupted when a key is pressed. - You'll have to press down to drag. - * Can't scroll while selecting text with mouse. * No scrollbars yet. That stuff is hard. ## Mirrors and Forks -Updates to lines.love can be downloaded from the following mirrors in addition -to the website above: -* https://git.sr.ht/~akkartik/lines.love -* https://repo.or.cz/lines.love.git -* https://tildegit.org/akkartik/lines.love -* https://git.merveilles.town/akkartik/lines.love -* https://git.tilde.institute/akkartik/lines.love -* https://codeberg.org/akkartik/lines.love -* https://github.com/akkartik/lines.love -* https://notabug.org/akkartik/lines.love -* https://pagure.io/lines.love -* https://nest.pijul.com/akkartik/lines.love (using the Pijul version control system) - -Forks of lines.love are encouraged. If you show me your fork, I'll link to it -here. - -* https://github.com/akkartik/lines-polygon-experiment -- an experiment that - uses separate shortcuts for regular polygons. `ctrl+3` for triangles, - `ctrl+4` for squares, etc. -* https://git.sr.ht/~akkartik/text.love -- a stripped down version without - drawings; useful starting point for some forks -* https://git.sr.ht/~akkartik/pensieve.love -- a note-taking app on an - infinite 2D surface. Still in development. -* https://git.sr.ht/~akkartik/capture.love -- a blank-slate mode for the - note-taking app, so all the stuff pensieve.love puts on screen doesn't cause - you to forget what you came to write down. - -## Associated tools - -* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown - and (non-editable) SVG. -* https://git.sr.ht/~akkartik/lines2html.love exports lines.love files to html - and inline SVG. +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) + +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) +[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 219689d..edfcb20 100644 --- a/edit.lua +++ b/edit.lua @@ -1,50 +1,18 @@ -- some constants people might like to tweak Text_color = {r=0, g=0, b=0} Cursor_color = {r=1, g=0, b=0} -Stroke_color = {r=0, g=0, b=0} -Current_stroke_color = {r=0.7, g=0.7, b=0.7} -- in process of being drawn -Current_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being edited -Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering over -Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text -Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings -Help_color = {r=0, g=0.5, b=0} -Help_background_color = {r=0, g=0.5, b=0, a=0.1} +Highlight_color = {r=0.7, g=0.7, b=0.9, a=0.4} -- selected text Margin_top = 15 Margin_left = 25 Margin_right = 25 -Drawing_padding_top = 10 -Drawing_padding_bottom = 10 -Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom - -Same_point_distance = 4 -- pixel distance at which two points are considered the same - edit = {} -- run in both tests and a real run function edit.initialize_state(top, left, right, font, font_height, line_height) -- currently always draws to bottom of screen local result = { - -- a line is either text or a drawing - -- a text is a table with: - -- mode = 'text', - -- string data, - -- a drawing is a table with: - -- mode = 'drawing' - -- a (h)eight, - -- an array of points, and - -- an array of shapes - -- a shape is a table containing: - -- a mode - -- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing) - -- an array vertices for mode 'polygon', 'rectangle', 'square' - -- p1, p2 for mode 'line' - -- center, radius for mode 'circle' - -- center, radius, start_angle, end_angle for mode 'arc' - -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide - -- The field names are carefully chosen so that switching modes in midstream - -- remembers previously entered points where that makes sense. - lines = {{mode='text', data=''}}, -- array of lines + lines = {{data=''}}, -- array of strings -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -78,9 +46,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height) cursor_x = 0, cursor_y = 0, - current_drawing_mode = 'line', -- one of the available shape modes - previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points - font = font, font_height = font_height, line_height = line_height, @@ -109,11 +74,9 @@ function edit.check_locs(State) -- throw away all cursor state entirely if edit.invalid1(State, State.screen_top1) or edit.invalid_cursor1(State) - or not edit.cursor_on_text(State) or not Text.le1(State.screen_top1, State.cursor1) then State.screen_top1 = {line=1, pos=1} State.cursor1 = {line=1, pos=1} - edit.put_cursor_on_next_text_line(State) end end @@ -134,27 +97,7 @@ function edit.invalid_cursor1(State) return cursor1.pos > #State.lines[cursor1.line].data + 1 end -function edit.cursor_on_text(State) - return State.cursor1.line <= #State.lines - and State.lines[State.cursor1.line].mode == 'text' -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 - State.cursor1.pos = 1 - end -end - --- return y drawn until function edit.draw(State) - State.button_handlers = {} love.graphics.setFont(State.font) App.color(Text_color) assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines)) @@ -167,46 +110,20 @@ function edit.draw(State) local line = State.lines[line_index] --? print('draw:', y, line_index, line) if y + State.line_height > App.screen.height then break end - if line.mode == 'text' then ---? print('text.draw', y, line_index) - local startpos = 1 - if line_index == State.screen_top1.line then - startpos = State.screen_top1.pos - end - if line.data == '' then - -- button to insert new drawing - button(State, 'draw', {x=State.left-Margin_left+4, y=y+4, w=12,h=12, bg={r=1,g=1,b=0}, - icon = icon.insert_drawing, - onpress1 = function() - Drawing.before = snapshot(State, line_index-1, line_index) - table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}}) - table.insert(State.line_cache, line_index, {}) - 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)}) - end, - }) - end - y = Text.draw(State, line_index, y, startpos) ---? print('=> y', y) - elseif line.mode == 'drawing' then - y = y+Drawing_padding_top - Drawing.draw(State, line_index, y) - y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom - else - assert(false, ('unknown line mode %s'):format(line.mode)) +--? print('text.draw', y, line_index) + local startpos = 1 + if line_index == State.screen_top1.line then + startpos = State.screen_top1.pos end + y = Text.draw(State, line_index, y, startpos) +--? print('=> y', y) end if State.search_term then Text.draw_search_bar(State) end - return y end function edit.update(State, dt) - Drawing.update(State, dt) if State.next_save and State.next_save < Current_time then save_to_disk(State) State.next_save = nil @@ -228,16 +145,10 @@ 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)) - if mouse_press_consumed_by_any_button(State, x,y, mouse_button) then - -- press on a button and it returned 'true' to short-circuit - return - end - if y < State.top then State.old_cursor1 = State.cursor1 State.old_selection1 = State.selection1 @@ -250,35 +161,25 @@ function edit.mouse_press(State, x,y, mouse_button) end for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - -- delicate dance between cursor, selection and old cursor/selection - -- scenarios: - -- regular press+release: sets cursor, clears selection - -- shift press+release: - -- sets selection to old cursor if not set otherwise leaves it untouched - -- sets cursor - -- press and hold to start a selection: sets selection on press, cursor on release - -- press and hold, then press shift: ignore shift - -- i.e. mouse_release should never look at shift state ---? print_and_log(('edit.mouse_press: in line %d'):format(line_index)) - State.old_cursor1 = State.cursor1 - State.old_selection1 = State.selection1 - State.mousepress_shift = App.shift_down() - State.selection1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } - return - end - elseif line.mode == 'drawing' then - if Drawing.in_drawing(State, line_index, x, y, State.left,State.right) then - 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) - return - end + if Text.in_line(State, line_index, x,y) then + -- delicate dance between cursor, selection and old cursor/selection + -- scenarios: + -- regular press+release: sets cursor, clears selection + -- shift press+release: + -- sets selection to old cursor if not set otherwise leaves it untouched + -- sets cursor + -- press and hold to start a selection: sets selection on press, cursor on release + -- press and hold, then press shift: ignore shift + -- i.e. mouse_release should never look at shift state +--? print_and_log(('edit.mouse_press: in line %d'):format(line_index)) + State.old_cursor1 = State.cursor1 + State.old_selection1 = State.selection1 + State.mousepress_shift = App.shift_down() + State.selection1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } + return end end @@ -286,48 +187,35 @@ 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: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos)) +--? 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 State.lines.current_drawing then - Drawing.mouse_release(State, x,y, mouse_button) - schedule_save(State) - if Drawing.before then - record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)}) - Drawing.before = nil - end - else ---? print_and_log('edit.mouse_release: no current drawing') - if y < State.top then - State.cursor1 = deepcopy(State.screen_top1) + if y < State.top then + State.cursor1 = deepcopy(State.screen_top1) + edit.clean_up_mouse_press(State) + return + end + for line_index,line in ipairs(State.lines) do + if Text.in_line(State, line_index, x,y) then +--? print_and_log(('edit.mouse_release: in line %d'):format(line_index)) + State.cursor1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos)) edit.clean_up_mouse_press(State) return end - - for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then ---? print_and_log(('edit.mouse_release: in line %d'):format(line_index)) - State.cursor1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } ---? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos)) - edit.clean_up_mouse_press(State) - return - end - end - end - - -- still here? mouse release is below all screen lines - State.cursor1 = Text.final_text_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 + + -- still here? mouse release is below all screen lines + 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 function edit.clean_up_mouse_press(State) @@ -347,13 +235,11 @@ end function edit.mouse_wheel_move(State, dx,dy) if dy > 0 then State.cursor1 = deepcopy(State.screen_top1) - edit.put_cursor_on_next_text_line(State) for i=1,math.floor(dy) do Text.up(State) end elseif dy < 0 then State.cursor1 = Text.screen_bottom1(State) - edit.put_cursor_on_next_text_line(State) for i=1,math.floor(-dy) do Text.down(State) end @@ -365,29 +251,19 @@ function edit.text_input(State, t) if State.search_term then State.search_term = State.search_term..t Text.search_next(State) - elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - p.name = p.name..t - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) else - local drawing_index, drawing = Drawing.current_drawing(State) - if drawing_index == nil then - Text.text_input(State, t) - end + 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 - 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 @@ -395,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 @@ -407,8 +283,10 @@ function edit.keychord_press(State, chord, key) State.screen_top = deepcopy(State.search_backup.screen_top) Text.search_next(State) elseif chord == 'down' then - State.cursor1.pos = State.cursor1.pos+1 - Text.search_next(State) + if #State.search_term > 0 then + Text.right(State) + Text.search_next(State) + end elseif chord == 'up' then Text.search_previous(State) end @@ -440,11 +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) - -- 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 @@ -455,10 +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) - -- 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 @@ -471,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 @@ -493,56 +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 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) - record_undo_event(State, {before=before, after=snapshot(State, drawing_index)}) - schedule_save(State) - end - elseif chord == 'escape' and not App.mouse_down(1) then - for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - line.show_help = false - end - end - elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then - if chord == 'return' then - State.current_drawing_mode = State.previous_drawing_mode - State.previous_drawing_mode = nil - else - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - if chord == 'escape' then - p.name = nil - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - elseif chord == 'backspace' then - local len = utf8.len(p.name) - if len > 0 then - local byte_offset = Text.offset(p.name, len-1) - if len == 1 then byte_offset = 0 end - p.name = string.sub(p.name, 1, byte_offset) - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - end - end - end 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 34be015..028ffb4 100644 --- a/file.lua +++ b/file.lua @@ -22,15 +22,11 @@ function load_from_file(infile) while true do local line = infile_next_line() if line == nil then break end - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated - table.insert(result, load_drawing(infile_next_line)) - else - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end @@ -41,148 +37,28 @@ function save_to_disk(State) error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - store_drawing(outfile, line) - else - outfile:write(line.data) - outfile:write('\n') - end + outfile:write(line.data) + outfile:write('\n') end outfile:close() end -function load_drawing(infile_next_line) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - while true do - local line = infile_next_line() - assert(line, 'drawing in file is incomplete') - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end - table.insert(drawing.shapes, shape) - end - return drawing -end - -function store_drawing(outfile, drawing) - outfile:write('```lines\n') - for _,shape in ipairs(drawing.shapes) do - if shape.mode == 'freehand' then - outfile:write(json.encode(shape)) - outfile:write('\n') - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]}) - outfile:write(line) - outfile:write('\n') - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - local obj = {mode=shape.mode, vertices={}} - for _,p in ipairs(shape.vertices) do - table.insert(obj.vertices, drawing.points[p]) - end - local line = json.encode(obj) - outfile:write(line) - outfile:write('\n') - elseif shape.mode == 'circle' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius})) - outfile:write('\n') - elseif shape.mode == 'arc' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle})) - outfile:write('\n') - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end - end - outfile:write('```\n') -end - -- for tests 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 ---? print(line) - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated ---? print('inserting drawing') - i, drawing = load_drawing_from_array(next_line, a, i) ---? print('i now', i) - table.insert(result, drawing) - else ---? print('inserting text') - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end -function load_drawing_from_array(iter, a, i) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - local line - while true do - i, line = iter(a, i) - assert(i, 'drawing in array is incomplete') ---? print(i) - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - assert(false, ('unknown drawing mode %s'):format(shape.mode)) - end - table.insert(drawing.shapes, shape) - end - return i, drawing -end - function is_absolute_path(path) local os_path_separator = package.config:sub(1,1) if os_path_separator == '/' then 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 232308e..83049c2 100644 --- a/main.lua +++ b/main.lua @@ -24,13 +24,6 @@ load_file_from_source_or_save_directory('button.lua') -- both sides require (different parts of) the logging framework load_file_from_source_or_save_directory('log.lua') --- both sides use drawings -load_file_from_source_or_save_directory('icons.lua') -load_file_from_source_or_save_directory('drawing.lua') - load_file_from_source_or_save_directory('geom.lua') - load_file_from_source_or_save_directory('help.lua') -load_file_from_source_or_save_directory('drawing_tests.lua') - -- but some files we want to only load sometimes function App.load() log_new('session') @@ -70,6 +63,11 @@ function App.load() load_file_from_source_or_save_directory('source_undo.lua') load_file_from_source_or_save_directory('colorize.lua') load_file_from_source_or_save_directory('source_text_tests.lua') + load_file_from_source_or_save_directory('icons.lua') + load_file_from_source_or_save_directory('drawing.lua') + load_file_from_source_or_save_directory('geom.lua') + load_file_from_source_or_save_directory('help.lua') + load_file_from_source_or_save_directory('drawing_tests.lua') load_file_from_source_or_save_directory('source_tests.lua') elseif current_app_is_warning() then else @@ -115,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..'"') @@ -210,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 @@ -254,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 @@ -296,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 @@ -322,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 1aadc41..80309c3 100644 --- a/reference.md +++ b/reference.md @@ -199,12 +199,9 @@ early warning if you break something. * `state = edit.initialize_state(top, left, right, font, font_height, line_height)` -- returns an object that can be used to render an interactive editor widget - for text and line drawings starting at `y=top` on the app window, between - `x=left` and `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. + for text starting at `y=top` on the app window, between `x=left` and + `x=right`. Wraps long lines at word boundaries where possible, or in the + middle of words (no hyphenation yet) when it must. * `edit.draw(state)` -- call this from `App.draw` to display the current editor state on the app window as requested in the call to @@ -248,20 +245,9 @@ Some constants that affect editor behavior: * `Margin_top`, `Margin_left`, `Margin_right` are integers in pixel units that affect where the editor is drawn on window (it always extends to bottom of window as needed) -* `Drawing_padding_top` and `Drawing_padding_bottom` affect spacing around - drawings. * Various color constants are represented as tables with r/g/b keys: * `Text_color`, `Cursor_color`, `Highlight_color` for drawing text. - * `Stroke_color`, `Current_stroke_color` for line drawings. - * `Icon_color` affects the color of the little mode icon on the top right of - a drawing. - * `Current_name_background_color` manages the color when naming points using - `ctrl+n`. - * `Focus_stroke_color` affects the color of a point or line when you hover - over it. - * `Help_color` and `Help_background_color` affect the color of online help - within line drawings. ### clickable buttons diff --git a/run.lua b/run.lua index 2cdd892..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() @@ -34,7 +34,7 @@ function run.initialize(arg) -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('lines.love - '..Editor_state.filename) + love.window.setTitle('text.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('lines.love - '..Editor_state.filename) + love.window.setTitle('text.love - '..Editor_state.filename) @@ -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 7e48274..78affdc 100644 --- a/select.lua +++ b/select.lua @@ -71,24 +71,22 @@ function Text.mouse_pos(State) return State.screen_top1.line, State.screen_top1.pos end for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - return line_index, Text.to_pos_on_line(State, line_index, x,y) - end + if Text.in_line(State, line_index, x,y) then + return line_index, Text.to_pos_on_line(State, line_index, x,y) end end local screen_bottom1 = Text.screen_bottom1(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) @@ -156,9 +154,7 @@ function Text.selection(State) assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl)) local result = {State.lines[minl].data:sub(min_offset)} for i=minl+1,maxl-1 do - if State.lines[i].mode == 'text' then - table.insert(result, State.lines[i].data) - end + table.insert(result, State.lines[i].data) end table.insert(result, State.lines[maxl].data:sub(1, max_offset-1)) return table.concat(result, '\n') diff --git a/source.lua b/source.lua index d24957c..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() @@ -74,7 +74,7 @@ function source.initialize() -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708 - love.window.setTitle('lines.love - source - '..Editor_state.filename) + love.window.setTitle('text.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('lines.love - source') + love.window.setTitle('text.love - source') @@ -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 34e12c3..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,17 +292,17 @@ 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 @@ -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 931dd00..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) @@ -427,7 +426,7 @@ function Text.pageup(State) State.screen_top1 = Text.previous_screen_top1(State) 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, @@ -476,7 +475,7 @@ function Text.pagedown(State) State.screen_top1 = Text.screen_bottom1(State) 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 @@ -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) @@ -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 e50b154..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) @@ -82,7 +82,6 @@ end function Text.populate_screen_line_starting_pos(State, line_index) local line = State.lines[line_index] - if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.screen_line_starting_pos then return @@ -124,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. @@ -141,7 +140,6 @@ function Text.text_input(State, t) end function Text.insert_at_cursor(State, t) - assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') 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) @@ -149,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) @@ -171,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 @@ -194,16 +191,11 @@ function Text.keychord_press(State, chord) end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) - if State.lines[State.cursor1.line-1].mode == 'drawing' then - table.remove(State.lines, State.cursor1.line-1) - table.remove(State.line_cache, State.cursor1.line-1) - else - -- 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) - end + -- 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 @@ -215,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 @@ -245,16 +237,14 @@ function Text.keychord_press(State, chord) -- no change to State.cursor1.pos end elseif State.cursor1.line < #State.lines then - if State.lines[State.cursor1.line+1].mode == 'text' then - -- join lines - State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - end + -- join lines + State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end Text.clear_screen_line_cache(State, State.cursor1.line) - schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) + schedule_save(State) --== shortcuts that move the cursor elseif chord == 'left' then Text.left(State) @@ -342,7 +332,7 @@ 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, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) + 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) @@ -353,7 +343,7 @@ function Text.pageup(State) State.screen_top1 = Text.previous_screen_top1(State) 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, @@ -365,15 +355,8 @@ function Text.starty(State, line_index) local loc2 = Text.to2(State, State.screen_top1) local y = State.top while true do - if State.lines[loc2.line].mode == 'drawing' then - y = y + Drawing_padding_top - end if loc2.line == line_index then return y end - if State.lines[loc2.line].mode == 'text' then - y = y + State.line_height - elseif State.lines[loc2.line].mode == 'drawing' then - y = y + Drawing.pixels(State.lines[loc2.line].h, State.width) + Drawing_padding_bottom - end + y = y + State.line_height if y + State.line_height > App.screen.height then break end local next_loc2 = Text.next_screen_line(State, loc2) if Text.eq2(next_loc2, loc2) then break end -- end of file @@ -388,11 +371,7 @@ function Text.previous_screen_top1(State) local y = App.screen.height - State.line_height while y >= State.top do if loc2.line == 1 and loc2.screen_line == 1 and loc2.screen_pos == 1 then break end - if State.lines[loc2.line].mode == 'text' then - y = y - State.line_height - elseif State.lines[loc2.line].mode == 'drawing' then - y = y - Drawing_padding_height - Drawing.pixels(State.lines[loc2.line].h, State.width) - end + y = y - State.line_height loc2 = Text.previous_screen_line(State, loc2) end return Text.to1(State, loc2) @@ -402,7 +381,7 @@ function Text.pagedown(State) State.screen_top1 = Text.screen_bottom1(State) 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 @@ -412,11 +391,7 @@ function Text.screen_bottom1(State) local loc2 = Text.to2(State, State.screen_top1) local y = State.top while true do - if State.lines[loc2.line].mode == 'text' then - y = y + State.line_height - elseif State.lines[loc2.line].mode == 'drawing' then - y = y + Drawing_padding_height + Drawing.pixels(State.lines[loc2.line].h, State.width) - end + y = y + State.line_height if y + State.line_height > App.screen.height then break end local next_loc2 = Text.next_screen_line(State, loc2) if Text.eq2(next_loc2, loc2) then break end @@ -426,29 +401,24 @@ function Text.screen_bottom1(State) end function Text.up(State) - assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) if screen_line_starting_pos == 1 then --? print('cursor is at first screen line of its line') -- line is done; skip to previous text line - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then ---? print('found previous text line') - State.cursor1 = {line=new_cursor_line, pos=nil} - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - -- previous text line found, pick its final screen line ---? print('has multiple screen lines') - local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos ---? print(#screen_line_starting_pos) - screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] - local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) - State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1 - break - end + if State.cursor1.line > 1 then + local new_cursor_line = State.cursor1.line-1 +--? print('found previous text line') + State.cursor1 = {line=new_cursor_line, pos=nil} + Text.populate_screen_line_starting_pos(State, State.cursor1.line) + -- previous text line found, pick its final screen line +--? print('has multiple screen lines') + local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos +--? print(#screen_line_starting_pos) + screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] + local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) + State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1 end else -- move up one screen line in current line @@ -464,28 +434,21 @@ 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 function Text.down(State) - assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) assert(State.cursor1.pos, 'cursor has no pos') if Text.cursor_at_final_screen_line(State) then -- line is done, skip to next text line --? print('cursor at final screen line of its line') - local new_cursor_line = State.cursor1.line - while new_cursor_line < #State.lines do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = { - line = new_cursor_line, - pos = Text.nearest_cursor_pos(State.font, State.lines[new_cursor_line].data, State.cursor_x, State.left), - } ---? print(State.cursor1.pos) - break - end + if State.cursor1.line < #State.lines then + local new_cursor_line = State.cursor1.line+1 + State.cursor1.line = new_cursor_line + State.cursor1.pos = Text.nearest_cursor_pos(State.font, State.lines[State.cursor1.line].data, State.cursor_x, State.left) +--? print(State.cursor1.pos) end local screen_bottom1 = Text.screen_bottom1(State) --? print('down 2', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, screen_bottom1.line, screen_bottom1.pos) @@ -589,28 +552,18 @@ function Text.match(s, pos, pat) end function Text.left(State) - assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') if State.cursor1.pos > 1 then State.cursor1.pos = State.cursor1.pos-1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = { - line = new_cursor_line, - pos = utf8.len(State.lines[new_cursor_line].data) + 1, - } - break - end - end + elseif State.cursor1.line > 1 then + State.cursor1.line = State.cursor1.line-1 + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 end if Text.lt1(State.cursor1, State.screen_top1) then State.screen_top1 = { line=State.cursor1.line, pos=Text.pos_at_start_of_screen_line(State, State.cursor1), } - Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks + Text.redraw_all(State) -- if we're scrolling, reclaim all line caches to avoid memory leaks end end @@ -622,18 +575,11 @@ function Text.right(State) end function Text.right_without_scroll(State) - assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text') if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then State.cursor1.pos = State.cursor1.pos+1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line <= #State.lines-1 do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = {line=new_cursor_line, pos=1} - break - end - end + elseif State.cursor1.line <= #State.lines-1 then + State.cursor1.line = State.cursor1.line+1 + State.cursor1.pos = 1 end end @@ -651,7 +597,6 @@ function Text.pos_at_start_of_screen_line(State, loc1) end function Text.pos_at_end_of_screen_line(State, loc1) - assert(State.lines[loc1.line].mode == 'text') Text.populate_screen_line_starting_pos(State, loc1.line) local line_cache = State.line_cache[loc1.line] local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1 @@ -665,23 +610,12 @@ 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) - if State.lines[screen_bottom1.line].mode == 'text' then - return { - line=screen_bottom1.line, - pos=Text.pos_at_end_of_screen_line(State, screen_bottom1), - } - end - local loc2 = Text.to2(State, screen_bottom1) - while true do - if State.lines[loc2.line].mode == 'text' then break end - assert(loc2.line > 1 or loc2.screen_line > 1 and loc2.screen_pos > 1) -- elsewhere we're making sure there's always at least one text line on screen - loc2 = Text.previous_screen_line(State, loc2) - end - local result = Text.to1(State, loc2) - result.pos = Text.pos_at_end_of_screen_line(State, result) - return result + return { + line=screen_bottom1.line, + pos=Text.pos_at_end_of_screen_line(State, screen_bottom1), + } end function Text.cursor_at_final_screen_line(State) @@ -692,26 +626,7 @@ function Text.cursor_at_final_screen_line(State) end function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - local y = State.top - while State.cursor1.line <= #State.lines do - if State.lines[State.cursor1.line].mode == 'text' then - break - end ---? print('cursor skips', State.cursor1.line) - y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width) - State.cursor1.line = State.cursor1.line + 1 - end - if State.cursor1.pos == nil then - State.cursor1.pos = 1 - end - -- hack: insert a text line at bottom of file if necessary - if State.cursor1.line > #State.lines then - assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed') - table.insert(State.lines, {mode='text', data=''}) - table.insert(State.line_cache, {}) - end ---? print(y, App.screen.height, App.screen.height-State.line_height) - if y > App.screen.height - State.line_height then + if State.top > App.screen.height - State.line_height then --? print('scroll up') Text.snap_cursor_to_bottom_of_screen(State) end @@ -731,31 +646,18 @@ function Text.snap_cursor_to_bottom_of_screen(State) while true do --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos) if top2.line == 1 and top2.screen_line == 1 then break end - if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then - local h = State.line_height - if y - h < State.top then - break - end - y = y - h - else - assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed') - assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not") - -- We currently can't draw partial drawings, so either skip it entirely - -- or not at all. - local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width) - if y - h < State.top then - break - end ---? print('skipping drawing of height', h) - y = y - h + local h = State.line_height + if y - h < State.top then + break end + y = y - h top2 = Text.previous_screen_line(State, top2) end --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) 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) @@ -911,9 +813,6 @@ function Text.x(font, s, pos) end function Text.to2(State, loc1) - if State.lines[loc1.line].mode == 'drawing' then - return {line=loc1.line, screen_line=1, screen_pos=1} - end local result = {line=loc1.line} local line_cache = State.line_cache[loc1.line] Text.populate_screen_line_starting_pos(State, loc1.line) @@ -979,8 +878,6 @@ function Text.previous_screen_line(State, loc2) return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1} elseif loc2.line == 1 then return loc2 - elseif State.lines[loc2.line-1].mode == 'drawing' then - return {line=loc2.line-1, screen_line=1, screen_pos=1} else local l = State.lines[loc2.line-1] Text.populate_screen_line_starting_pos(State, loc2.line-1) @@ -989,9 +886,6 @@ function Text.previous_screen_line(State, loc2) end function Text.next_screen_line(State, loc2) - if State.lines[loc2.line].mode == 'drawing' then - return {line=loc2.line+1, screen_line=1, screen_pos=1} - end Text.populate_screen_line_starting_pos(State, loc2.line) if loc2.screen_line >= #State.line_cache[loc2.line].screen_line_starting_pos then if loc2.line < #State.lines then @@ -1032,7 +926,7 @@ function Text.tweak_screen_top_and_cursor(State) 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 @@ -1044,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 9b34a24..962de7d 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -15,32 +15,6 @@ function test_initial_state() check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos') end -function test_click_to_create_drawing() - App.screen.init{width=800, height=600} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1) - -- cursor skips drawing to always remain on text - check_eq(#Editor_state.lines, 2, '#lines') - check_eq(Editor_state.cursor1.line, 2, 'cursor') -end - -function test_backspace_to_delete_drawing() - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - -- cursor is on text as always (outside tests this will get initialized correctly) - Editor_state.cursor1.line = 2 - -- backspacing deletes the drawing - edit.run_after_keychord(Editor_state, 'backspace', 'backspace') - check_eq(#Editor_state.lines, 1, '#lines') - check_eq(Editor_state.cursor1.line, 1, 'cursor') -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} @@ -319,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() @@ -331,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 @@ -994,34 +969,6 @@ function test_pagedown() App.screen.check(y, 'ghi', 'screen:2') end -function test_pagedown_skips_drawings() - -- some lines of text with a drawing intermixed - local drawing_width = 50 - App.screen.init{width=Editor_state.left+drawing_width, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', -- height 15 - '```lines', '```', -- height 25 - 'def', -- height 15 - 'ghi'} -- height 15 - Text.redraw_all(Editor_state) - check_eq(Editor_state.lines[2].mode, 'drawing', 'baseline/lines') - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - local drawing_height = Drawing_padding_height + drawing_width/2 -- default - -- initially the screen displays the first line and the drawing - -- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80px - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'baseline/screen:1') - -- after pagedown the screen draws the drawing up top - -- 15px margin + 10px margin + 25px drawing + 10px margin + 15px line3 = 75px < screen height 80px - edit.run_after_keychord(Editor_state, 'pagedown', 'pagedown') - check_eq(Editor_state.screen_top1.line, 2, 'screen_top') - check_eq(Editor_state.cursor1.line, 3, 'cursor') - y = Editor_state.top + drawing_height - App.screen.check(y, 'def', 'screen:1') -end - function test_pagedown_can_start_from_middle_of_long_wrapping_line() -- draw a few lines starting from a very long wrapping line App.screen.init{width=Editor_state.left+30, height=60} @@ -1099,30 +1046,6 @@ function test_down_arrow_moves_cursor() App.screen.check(y, 'ghi', 'screen:3') end -function test_down_arrow_skips_drawing() - -- some lines of text with a drawing intermixed - local drawing_width = 50 - App.screen.init{width=Editor_state.left+drawing_width, height=100} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', -- height 15 - '```lines', '```', -- height 25 - 'ghi'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=1, pos=1} - 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 - local drawing_height = Drawing_padding_height + drawing_width/2 -- default - y = y + drawing_height - App.screen.check(y, 'ghi', 'baseline/screen:3') - check(Editor_state.cursor_x, 'baseline/cursor_x') - -- after hitting the down arrow the cursor moves down by 2 lines, skipping the drawing - edit.run_after_keychord(Editor_state, 'down', 'down') - check_eq(Editor_state.cursor1.line, 3, 'cursor') -end - function test_down_arrow_scrolls_down_by_one_line() -- display the first three lines with the cursor on the bottom line App.screen.init{width=120, height=60} @@ -1266,30 +1189,6 @@ function test_up_arrow_moves_cursor() App.screen.check(y, 'ghi', 'screen:3') end -function test_up_arrow_skips_drawing() - -- some lines of text with a drawing intermixed - local drawing_width = 50 - App.screen.init{width=Editor_state.left+drawing_width, height=100} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', -- height 15 - '```lines', '```', -- height 25 - 'ghi'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=1} - 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 - local drawing_height = Drawing_padding_height + drawing_width/2 -- default - y = y + drawing_height - App.screen.check(y, 'ghi', 'baseline/screen:3') - check(Editor_state.cursor_x, 'baseline/cursor_x') - -- after hitting the up arrow the cursor moves up by 2 lines, skipping the drawing - edit.run_after_keychord(Editor_state, 'up', 'up') - check_eq(Editor_state.cursor1.line, 1, 'cursor') -end - function test_up_arrow_scrolls_up_by_one_line() -- display the lines 2/3/4 with the cursor on line 2 App.screen.init{width=120, height=60} @@ -1317,27 +1216,6 @@ function test_up_arrow_scrolls_up_by_one_line() App.screen.check(y, 'ghi', 'screen:3') end -function test_up_arrow_scrolls_up_by_one_line_skipping_drawing() - -- display lines 3/4/5 with a drawing just off screen at line 2 - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', '```lines', '```', 'def', 'ghi', 'jkl'} - Text.redraw_all(Editor_state) - Editor_state.cursor1 = {line=3, pos=1} - Editor_state.screen_top1 = {line=3, 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 the up arrow the screen scrolls up to previous text line - edit.run_after_keychord(Editor_state, 'up', 'up') - check_eq(Editor_state.screen_top1.line, 1, 'screen_top') - check_eq(Editor_state.cursor1.line, 1, 'cursor') -end - function test_up_arrow_scrolls_up_by_one_screen_line() -- display lines starting from second screen line of a line App.screen.init{width=Editor_state.left+30, height=60} @@ -2000,7 +1878,7 @@ end function test_search() App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', '’deg'} -- contains unicode quote in final line + Editor_state.lines = load_array{'abc', 'def', 'ghi', '’deg'} -- contains unicode quote in final line Text.redraw_all(Editor_state) Editor_state.cursor1 = {line=1, pos=1} Editor_state.screen_top1 = {line=1, pos=1} @@ -2073,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 e5dea93..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 @@ -50,23 +49,13 @@ function snapshot(State, s,e) screen_top=deepcopy(State.screen_top1), selection=deepcopy(State.selection1), cursor=deepcopy(State.cursor1), - current_drawing_mode=Drawing_mode, - previous_drawing_mode=State.previous_drawing_mode, lines={}, start_line=s, end_line=e, -- no filename; undo history is cleared when filename changes } - -- deep copy lines without cached stuff like text fragments for i=s,e do - local line = State.lines[i] - 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 +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 |