Merge lines.love

[?]
Sep 5, 2022, 7:47 PM
3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC

Dependencies

  • [2] 32V6ZHQB Merge lines.love
  • [3] AKZWDWIA Merge lines.love
  • [4] SXTQMD44 include some missing source files
  • [5] OI4FPFIN support drawings in the source editor
  • [6] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing
  • [7] KECEMMMR extract couple of functions
  • [8] 66X36NZN a little more prose describing manual_tests
  • [9] MUJTM6RE bring back a level of wrapping
  • [10] Z5HLXU4P add state arg to a few functions
  • [11] NP7PIUBT bugfix: restore state after C-f (find)
  • [12] VXORMHME delete experimental REPL
  • [13] 3QNOKBFM beginnings of a test harness
  • [14] FS2ITYYH record a known issue
  • [15] KKMFQDR4 editing source code from within the app
  • [16] ODLKHO7B switch to line index in a function
  • [17] 2CTN2IEF Merge lines.love
  • [18] OTIBCAUJ love2d scaffold
  • [19] SPNMXTYR have file API operate on state object
  • [20] K464QQR4 more defensive resize handling
  • [21] 3QQZ7W4E bring couple more globals back to the app level
  • [22] TLOAPLBJ add a license
  • [23] EAEGCJV5 rename
  • [24] HTWAM4NZ bugfix: scrolling in left/right movements
  • [25] F65ADDGL add state arg to a few functions
  • [26] X3CQLBTR set window title within each app
  • [27] XX7G2FFJ intermingle freehand line drawings with text
  • [28] 2L5MEZV3 experiment: new edit namespace
  • [29] 2RXZ3PGO beginning of a new approach to scroll+wrap
  • [30] AVTNUQYR basic test-enabled framework
  • [31] UN7L3DNN avoid some string concatenations
  • [32] MD3W5IRA new fork: rip out drawing support
  • [33] R6GUSTBY default font size and line-height
  • [34] BLWAYPKV extract a module
  • [35] E4HEHLRT extract a variable
  • [36] SDRXK4X5 move
  • [37] TVCPXAAU rename
  • [38] GUOQRUL7 Merge lines.love
  • [39] VHQCNMAR several more modules
  • [40] SVJZZDC3 snapshot - no, that's all wrong
  • [41] PX3736DX better error message
  • [42] DCO5BQWV Merge lines.love
  • [43] 73OCE2MC after much struggle, a brute-force undo
  • [44] WJBZZQE4 fold together two largely similar cases
  • [45] JOPVPUSA editing source code from within the app
  • [46] T4FRZSYL delete an ancient, unused file
  • [47] LNUHQOGH start passing in Editor_state explicitly
  • [48] 5USLYLBV duplicate
  • [49] LXTTOB33 extract a couple of files
  • [50] D2GCFTTT clean up repl functionality
  • [51] 4YDBYBA4 clean up memory leak experiments
  • [52] YGCT2D2O start loading settings as applicable
  • [53] LF7BWEG4 group all editor globals
  • [54] Z4XRNDTR find text
  • [55] RSZD5A7G forgot to add json.lua
  • [56] 6LJZN727 handle chords
  • [57] CE4LZV4T drop last couple of manual tests
  • [58] R5QXEHUI somebody stop me
  • [59] EGH7XDBK support non-text lines in Text.to2
  • [60] ZPUQSPQP extract a few methods
  • [61] TXDMRA5J bugfix: alt-tab shouldn't emit keypress events
  • [62] BULPIBEG beginnings of a module for the text editor
  • [63] QS3YLNKZ Merge lines.love
  • [64] K2X6G75Z start writing some tests for drawings
  • [65] OGUV4HSA remove some memory leaks from rendered fragments

Change contents

  • file deletion: source_undo.lua (----------)source_undo.lua (----------)
    [6.2][6.3457:3496](),[6.2][6.3457:3496](),[6.3496][6.5:5]()
    if line.mode == 'text' then
    table.insert(event.lines, {mode='text', data=line.data, dataB=line.dataB})
    elseif line.mode == 'drawing' then
    local points=deepcopy(line.points)
    --? print('copying', line.points, 'with', #line.points, 'points into', points)
    local shapes=deepcopy(line.shapes)
    --? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)
    table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}})
    --? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
    else
    print(line.mode)
    assert(false)
    end
    end
    return event
    end
    function patch(lines, from, to)
    --? if #from.lines == 1 and #to.lines == 1 then
    --? assert(from.start_line == from.end_line)
    --? assert(to.start_line == to.end_line)
    --? assert(from.start_line == to.start_line)
    --? lines[from.start_line] = to.lines[1]
    --? return
    --? end
    assert(from.start_line == to.start_line)
    for i=from.end_line,from.start_line,-1 do
    table.remove(lines, i)
    end
    assert(#to.lines == to.end_line-to.start_line+1)
    for i=1,#to.lines do
    table.insert(lines, to.start_line+i-1, to.lines[i])
    end
    end
    function patch_placeholders(line_cache, from, to)
    assert(from.start_line == to.start_line)
    for i=from.end_line,from.start_line,-1 do
    table.remove(line_cache, i)
    end
    assert(#to.lines == to.end_line-to.start_line+1)
    for i=1,#to.lines do
    table.insert(line_cache, to.start_line+i-1, {})
    end
    end
    -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
    function deepcopy(obj, seen)
    if type(obj) ~= 'table' then return obj end
    if seen and seen[obj] then return seen[obj] end
    local s = seen or {}
    local result = setmetatable({}, getmetatable(obj))
    s[obj] = result
    for k,v in pairs(obj) do
    result[deepcopy(k, s)] = deepcopy(v, s)
    end
    return result
    end
    function minmax(a, b)
    return math.min(a,b), math.max(a,b)
    end
    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]
  • file deletion: source_text_tests.lua (----------)source_text_tests.lua (----------)
    [6.2][6.83676:83721](),[6.2][6.83676:83721](),[6.83721][6.3498:3498]()
    Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- search for a string
    edit.run_after_keychord(Editor_state, 'C-f')
    edit.run_after_textinput(Editor_state, 'd')
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_search/1/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/1/cursor:pos')
    -- reset cursor
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    -- search for second occurrence
    edit.run_after_keychord(Editor_state, 'C-f')
    edit.run_after_textinput(Editor_state, 'de')
    edit.run_after_keychord(Editor_state, 'down')
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.cursor1.line, 4, 'F - test_search/2/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/2/cursor:pos')
    end
    function test_search_upwards()
    io.write('\ntest_search_upwards')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc abd'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- search for a string
    edit.run_after_keychord(Editor_state, 'C-f')
    edit.run_after_textinput(Editor_state, 'a')
    -- search for previous occurrence
    edit.run_after_keychord(Editor_state, 'up')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_search_upwards/2/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_upwards/2/cursor:pos')
    end
    function test_search_wrap()
    io.write('\ntest_search_wrap')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=3}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- search for a string
    edit.run_after_keychord(Editor_state, 'C-f')
    edit.run_after_textinput(Editor_state, 'a')
    edit.run_after_keychord(Editor_state, 'return')
    -- cursor wraps
    check_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap/1/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_wrap/1/cursor:pos')
    end
    function test_search_wrap_upwards()
    io.write('\ntest_search_wrap_upwards')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc abd'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- search upwards for a string
    edit.run_after_keychord(Editor_state, 'C-f')
    edit.run_after_textinput(Editor_state, 'a')
    edit.run_after_keychord(Editor_state, 'up')
    -- cursor wraps
    check_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap_upwards/1/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_search_wrap_upwards/1/cursor:pos')
    end
    end
    function test_pagedown_skips_drawings()
    io.write('\ntest_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', 'F - test_pagedown_skips_drawings/baseline/lines')
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    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', 'F - test_pagedown_skips_drawings/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')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown_skips_drawings/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_skips_drawings/cursor')
    y = Editor_state.top + drawing_height
    App.screen.check(y, 'def', 'F - test_pagedown_skips_drawings/screen:1')
    end
    function test_pagedown_can_start_from_middle_of_long_wrapping_line()
    io.write('\ntest_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}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu vwx yza bcd efg hij', 'XYZ'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:3')
    -- after pagedown we scroll down the very long wrapping line
    edit.run_after_keychord(Editor_state, 'pagedown')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:line')
    check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:pos')
    y = Editor_state.top
    App.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')
    end
    function test_pagedown_never_moves_up()
    io.write('\ntest_pagedown_never_moves_up')
    -- draw the final screen line of a wrapping line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def ghi'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=9}
    Editor_state.screen_top1 = {line=1, pos=9}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- pagedown makes no change
    edit.run_after_keychord(Editor_state, 'pagedown')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_never_moves_up/screen_top:line')
    check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_never_moves_up/screen_top:pos')
    end
    function test_down_arrow_moves_cursor()
    io.write('\ntest_down_arrow_moves_cursor')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- initially the first three lines are displayed
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/baseline/screen:3')
    -- after hitting the down arrow, the cursor moves down by 1 line
    edit.run_after_keychord(Editor_state, 'down')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_down_arrow_moves_cursor/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_down_arrow_moves_cursor/cursor')
    -- the screen is unchanged
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/screen:3')
    end
    function test_down_arrow_scrolls_down_by_one_line()
    io.write('\ntest_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}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:3')
    -- after hitting the down arrow the screen scrolls down by one line
    edit.run_after_keychord(Editor_state, 'down')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_line/screen_top')
    check_eq(Editor_state.cursor1.line, 4, 'F - test_down_arrow_scrolls_down_by_one_line/cursor')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_line/screen:3')
    end
    function test_down_arrow_scrolls_down_by_one_screen_line()
    io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line')
    -- display the first three lines with the cursor on the bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace
    -- after hitting the down arrow the screen scrolls down by one line
    edit.run_after_keychord(Editor_state, 'down')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:3')
    end
    function test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word()
    io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word')
    -- display the first three lines with the cursor on the bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:3')
    -- after hitting the down arrow the screen scrolls down by one line
    edit.run_after_keychord(Editor_state, 'down')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'kl', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:3')
    end
    function test_page_down_followed_by_down_arrow_does_not_scroll_screen_up()
    io.write('\ntest_page_down_followed_by_down_arrow_does_not_scroll_screen_up')
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:3')
    -- after hitting pagedown the screen scrolls down to start of a long line
    edit.run_after_keychord(Editor_state, 'pagedown')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:pos')
    -- after hitting down arrow the screen doesn't scroll down further, and certainly doesn't scroll up
    edit.run_after_keychord(Editor_state, 'down')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'kl', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:3')
    end
    function test_up_arrow_moves_cursor()
    io.write('\ntest_up_arrow_moves_cursor')
    -- display the first 3 lines with the cursor on the bottom line
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/baseline/screen:3')
    -- after hitting the up arrow the cursor moves up by 1 line
    edit.run_after_keychord(Editor_state, 'up')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_moves_cursor/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_up_arrow_moves_cursor/cursor')
    -- the screen is unchanged
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/screen:3')
    end
    function test_up_arrow_scrolls_up_by_one_line()
    io.write('\ntest_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}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:3')
    -- after hitting the up arrow the screen scrolls up by one line
    edit.run_after_keychord(Editor_state, 'up')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/cursor')
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_by_one_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/screen:3')
    end
    function test_up_arrow_scrolls_up_by_one_screen_line()
    io.write('\ntest_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}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=6}
    Editor_state.screen_top1 = {line=3, pos=5}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:2')
    -- after hitting the up arrow the screen scrolls up to first screen line
    edit.run_after_keychord(Editor_state, 'up')
    y = Editor_state.top
    App.screen.check(y, 'ghi ', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:3')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')
    check_eq(Editor_state.screen_top1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:pos')
    end
    function test_up_arrow_scrolls_up_to_final_screen_line()
    io.write('\ntest_up_arrow_scrolls_up_to_final_screen_line')
    -- display lines starting just after a long line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:3')
    -- after hitting the up arrow the screen scrolls up to final screen line of previous line
    edit.run_after_keychord(Editor_state, 'up')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:3')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')
    check_eq(Editor_state.screen_top1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:pos')
    end
    function test_up_arrow_scrolls_up_to_empty_line()
    io.write('\ntest_up_arrow_scrolls_up_to_empty_line')
    -- display a screenful of text with an empty line just above it outside the screen
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'', 'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:3')
    -- after hitting the up arrow the screen scrolls up by one line
    edit.run_after_keychord(Editor_state, 'up')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/cursor')
    y = Editor_state.top
    -- empty first line
    y = y + Editor_state.line_height
    App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:3')
    end
    function test_pageup()
    io.write('\ntest_pageup')
    App.screen.init{width=120, height=45}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    -- initially the last two lines are displayed
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_pageup/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_pageup/baseline/screen:2')
    -- after pageup the cursor goes to first line
    edit.run_after_keychord(Editor_state, 'pageup')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup/cursor')
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_pageup/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_pageup/screen:2')
    end
    function test_pageup_scrolls_up_by_screen_line()
    io.write('\ntest_pageup_scrolls_up_by_screen_line')
    -- display the first three lines with the cursor on the bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace
    -- after hitting the page-up key the screen scrolls up to top
    edit.run_after_keychord(Editor_state, 'pageup')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_by_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_by_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/screen:3')
    end
    function test_pageup_scrolls_up_from_middle_screen_line()
    io.write('\ntest_pageup_scrolls_up_from_middle_screen_line')
    -- display a few lines starting from the middle of a line (Editor_state.cursor1.pos > 1)
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=5}
    Editor_state.screen_top1 = {line=2, pos=5}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace
    -- after hitting the page-up key the screen scrolls up to top
    edit.run_after_keychord(Editor_state, 'pageup')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3')
    end
    function test_enter_on_bottom_line_scrolls_down()
    io.write('\ntest_enter_on_bottom_line_scrolls_down')
    -- display a few lines with cursor on bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')
    -- after hitting the enter key the screen scrolls down
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_enter_on_bottom_line_scrolls_down/screen_top')
    check_eq(Editor_state.cursor1.line, 4, 'F - test_enter_on_bottom_line_scrolls_down/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_bottom_line_scrolls_down/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3')
    end
    function test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()
    io.write('\ntest_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom')
    -- display just the bottom line on screen
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=4, pos=2}
    Editor_state.screen_top1 = {line=4, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/baseline/screen:1')
    -- after hitting the enter key the screen does not scroll down
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.screen_top1.line, 4, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')
    check_eq(Editor_state.cursor1.line, 5, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')
    end
    function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()
    io.write('\ntest_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom')
    -- display just an empty bottom line on screen
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', ''}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- after hitting the inserting_text key the screen does not scroll down
    edit.run_after_textinput(Editor_state, 'a')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')
    local y = Editor_state.top
    App.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
    end
    function test_typing_on_bottom_line_scrolls_down()
    io.write('\ntest_typing_on_bottom_line_scrolls_down')
    -- display a few lines with cursor on bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=4}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:3')
    -- after typing something the line wraps and the screen scrolls down
    edit.run_after_textinput(Editor_state, 'j')
    edit.run_after_textinput(Editor_state, 'k')
    edit.run_after_textinput(Editor_state, 'l')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_typing_on_bottom_line_scrolls_down/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_typing_on_bottom_line_scrolls_down/cursor:line')
    check_eq(Editor_state.cursor1.pos, 7, 'F - test_typing_on_bottom_line_scrolls_down/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')
    end
    function test_left_arrow_scrolls_up_in_wrapped_line()
    io.write('\ntest_left_arrow_scrolls_up_in_wrapped_line')
    -- display lines starting from second screen line of a line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=3, pos=5}
    Editor_state.screen_bottom1 = {}
    -- cursor is at top of screen
    Editor_state.cursor1 = {line=3, pos=5}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:2')
    -- after hitting the left arrow the screen scrolls up to first screen line
    edit.run_after_keychord(Editor_state, 'left')
    y = Editor_state.top
    App.screen.check(y, 'ghi ', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:3')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')
    check_eq(Editor_state.screen_top1.pos, 1, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:pos')
    end
    function test_right_arrow_scrolls_down_in_wrapped_line()
    io.write('\ntest_right_arrow_scrolls_down_in_wrapped_line')
    -- display the first three lines with the cursor on the bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- cursor is at bottom right of screen
    Editor_state.cursor1 = {line=3, pos=5}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace
    -- after hitting the right arrow the screen scrolls down by one line
    edit.run_after_keychord(Editor_state, 'right')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 6, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:3')
    end
    function test_home_scrolls_up_in_wrapped_line()
    io.write('\ntest_home_scrolls_up_in_wrapped_line')
    -- display lines starting from second screen line of a line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=3, pos=5}
    Editor_state.screen_bottom1 = {}
    -- cursor is at top of screen
    Editor_state.cursor1 = {line=3, pos=5}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:2')
    -- after hitting home the screen scrolls up to first screen line
    edit.run_after_keychord(Editor_state, 'home')
    y = Editor_state.top
    App.screen.check(y, 'ghi ', 'F - test_home_scrolls_up_in_wrapped_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/screen:3')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')
    check_eq(Editor_state.screen_top1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/cursor:pos')
    end
    function test_end_scrolls_down_in_wrapped_line()
    io.write('\ntest_end_scrolls_down_in_wrapped_line')
    -- display the first three lines with the cursor on the bottom line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- cursor is at bottom right of screen
    Editor_state.cursor1 = {line=3, pos=5}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace
    -- after hitting end the screen scrolls down by one line
    edit.run_after_keychord(Editor_state, 'end')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_end_scrolls_down_in_wrapped_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_end_scrolls_down_in_wrapped_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 8, 'F - test_end_scrolls_down_in_wrapped_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')
    end
    function test_position_cursor_on_recently_edited_wrapping_line()
    -- draw a line wrapping over 2 screen lines
    io.write('\ntest_position_cursor_on_recently_edited_wrapping_line')
    App.screen.init{width=100, height=200}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=25}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'xyz', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:3')
    -- add to the line until it's wrapping over 3 screen lines
    edit.run_after_textinput(Editor_state, 's')
    edit.run_after_textinput(Editor_state, 't')
    edit.run_after_textinput(Editor_state, 'u')
    check_eq(Editor_state.cursor1.pos, 28, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3')
    -- try to move the cursor earlier in the third screen line by clicking the mouse
    edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)
    -- cursor should move
    check_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')
    end
    function test_backspace_can_scroll_up()
    io.write('\ntest_backspace_can_scroll_up')
    -- display the lines 2/3/4 with the cursor on line 2
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')
    -- after hitting backspace the screen scrolls up by one line
    edit.run_after_keychord(Editor_state, 'backspace')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_can_scroll_up/screen_top')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_can_scroll_up/cursor')
    y = Editor_state.top
    App.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3')
    end
    function test_backspace_can_scroll_up_screen_line()
    io.write('\ntest_backspace_can_scroll_up_screen_line')
    -- display lines starting from second screen line of a line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=5}
    Editor_state.screen_top1 = {line=3, pos=5}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:2')
    -- after hitting backspace the screen scrolls up by one screen line
    edit.run_after_keychord(Editor_state, 'backspace')
    y = Editor_state.top
    App.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3')
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/screen_top')
    check_eq(Editor_state.screen_top1.pos, 1, 'F - test_backspace_can_scroll_up_screen_line/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_backspace_can_scroll_up_screen_line/cursor:pos')
    end
    function test_backspace_past_line_boundary()
    io.write('\ntest_backspace_past_line_boundary')
    -- position cursor at start of a (non-first) line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    -- backspace joins with previous line
    edit.run_after_keychord(Editor_state, 'backspace')
    check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")
    end
    function test_undo_insert_text()
    io.write('\ntest_undo_insert_text')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=4}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- insert a character
    edit.draw(Editor_state)
    edit.run_after_textinput(Editor_state, 'g')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos')
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/cursor:line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')
    end
    function test_undo_delete_text()
    io.write('\ntest_undo_delete_text')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'defg', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=5}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- delete a character
    edit.run_after_keychord(Editor_state, 'backspace')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos')
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')
    -- undo
    --? -- after undo, the backspaced key is selected
    edit.run_after_keychord(Editor_state, 'C-z')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
    end
    function test_search()
    io.write('\ntest_search')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    end
    function test_click_to_create_drawing()
    io.write('\ntest_click_to_create_drawing')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)
    -- cursor skips drawing to always remain on text
    check_eq(#Editor_state.lines, 2, 'F - test_click_to_create_drawing/#lines')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_click_to_create_drawing/cursor')
    end
    function test_backspace_to_delete_drawing()
    io.write('\ntest_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')
    check_eq(#Editor_state.lines, 1, 'F - test_backspace_to_delete_drawing/#lines')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_to_delete_drawing/cursor')
    end
    function test_backspace_from_start_of_final_line()
    io.write('\ntest_backspace_from_start_of_final_line')
    -- display final line of text with cursor at start of it
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.cursor1 = {line=2, pos=1}
    Text.redraw_all(Editor_state)
    -- backspace scrolls up
    edit.run_after_keychord(Editor_state, 'backspace')
    check_eq(#Editor_state.lines, 1, 'F - test_backspace_from_start_of_final_line/#lines')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_from_start_of_final_line/cursor')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_from_start_of_final_line/screen_top')
    end
    function test_insert_first_character()
    io.write('\ntest_insert_first_character')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    edit.run_after_textinput(Editor_state, 'a')
    local y = Editor_state.top
    App.screen.check(y, 'a', 'F - test_insert_first_character/screen:1')
    end
    function test_press_ctrl()
    io.write('\ntest_press_ctrl')
    -- press ctrl while the cursor is on text
    App.screen.init{width=50, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{''}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.run_after_keychord(Editor_state, 'C-m')
    end
    function test_move_left()
    io.write('\ntest_move_left')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'a'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=2}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'left')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_left')
    end
    function test_move_right()
    io.write('\ntest_move_right')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'a'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'right')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_right')
    end
    function test_move_left_to_previous_line()
    io.write('\ntest_move_left_to_previous_line')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'left')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_move_left_to_previous_line/line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_left_to_previous_line/pos') -- past end of line
    end
    function test_move_right_to_next_line()
    io.write('\ntest_move_right_to_next_line')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=4} -- past end of line
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'right')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_move_right_to_next_line/line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_right_to_next_line/pos')
    end
    function test_move_to_start_of_word()
    io.write('\ntest_move_to_start_of_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=3}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_to_start_of_word')
    end
    function test_move_to_start_of_previous_word()
    io.write('\ntest_move_to_start_of_previous_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=4} -- at the space between words
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_to_start_of_previous_word')
    end
    function test_skip_to_previous_word()
    io.write('\ntest_skip_to_previous_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=5} -- at the start of second word
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_to_previous_word')
    end
    function test_skip_past_tab_to_previous_word()
    io.write('\ntest_skip_past_tab_to_previous_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def\tghi'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=10} -- within third word
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_past_tab_to_previous_word')
    end
    function test_skip_multiple_spaces_to_previous_word()
    io.write('\ntest_skip_multiple_spaces_to_previous_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=6} -- at the start of second word
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_multiple_spaces_to_previous_word')
    end
    function test_move_to_start_of_word_on_previous_line()
    io.write('\ntest_move_to_start_of_word_on_previous_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=2, pos=1}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-left')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_move_to_start_of_word_on_previous_line/line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_move_to_start_of_word_on_previous_line/pos')
    end
    function test_move_past_end_of_word()
    io.write('\ntest_move_past_end_of_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-right')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word')
    end
    function test_skip_to_next_word()
    io.write('\ntest_skip_to_next_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=4} -- at the space between words
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-right')
    check_eq(Editor_state.cursor1.pos, 8, 'F - test_skip_to_next_word')
    end
    function test_skip_past_tab_to_next_word()
    io.write('\ntest_skip_past_tab_to_next_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc\tdef'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1} -- at the space between words
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-right')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_skip_past_tab_to_next_word')
    end
    function test_skip_multiple_spaces_to_next_word()
    io.write('\ntest_skip_multiple_spaces_to_next_word')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=4} -- at the start of second word
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-right')
    check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_multiple_spaces_to_next_word')
    end
    function test_move_past_end_of_word_on_next_line()
    io.write('\ntest_move_past_end_of_word_on_next_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=8}
    edit.draw(Editor_state)
    edit.run_after_keychord(Editor_state, 'M-right')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_move_past_end_of_word_on_next_line/line')
    check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word_on_next_line/pos')
    end
    function test_click_with_mouse()
    io.write('\ntest_click_with_mouse')
    -- display two lines with cursor on one of them
    App.screen.init{width=50, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click on the other line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    -- cursor moves
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')
    end
    function test_click_with_mouse_to_left_of_line()
    io.write('\ntest_click_with_mouse_to_left_of_line')
    -- display a line with the cursor in the middle
    App.screen.init{width=50, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=3}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click to the left of the line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)
    -- cursor moves to start of line
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:pos')
    end
    function test_click_with_mouse_takes_margins_into_account()
    io.write('\ntest_click_with_mouse_takes_margins_into_account')
    -- display two lines with cursor on one of them
    App.screen.init{width=100, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.left = 50 -- occupy only right side of screen
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click on the other line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    -- cursor moves
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_takes_margins_into_account/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_takes_margins_into_account/cursor:pos')
    end
    function test_click_with_mouse_on_empty_line()
    io.write('\ntest_click_with_mouse_on_empty_line')
    -- display two lines with the first one empty
    App.screen.init{width=50, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click on the empty line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    -- cursor moves
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_empty_line/cursor')
    end
    function test_draw_text()
    io.write('\ntest_draw_text')
    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=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_draw_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_draw_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_draw_text/screen:3')
    end
    function test_draw_wrapping_text()
    io.write('\ntest_draw_wrapping_text')
    App.screen.init{width=50, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_draw_wrapping_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'de', 'F - test_draw_wrapping_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'fgh', 'F - test_draw_wrapping_text/screen:3')
    end
    function test_draw_word_wrapping_text()
    io.write('\ntest_draw_word_wrapping_text')
    App.screen.init{width=60, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc ', 'F - test_draw_word_wrapping_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def ', 'F - test_draw_word_wrapping_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_draw_word_wrapping_text/screen:3')
    end
    function test_click_with_mouse_on_wrapping_line()
    io.write('\ntest_click_with_mouse_on_wrapping_line')
    -- display two lines with cursor on one of them
    App.screen.init{width=50, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=20}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click on the other line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    -- cursor moves
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line/cursor:pos')
    end
    function test_click_with_mouse_on_wrapping_line_takes_margins_into_account()
    io.write('\ntest_click_with_mouse_on_wrapping_line_takes_margins_into_account')
    -- display two lines with cursor on one of them
    App.screen.init{width=100, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.left = 50 -- occupy only right side of screen
    Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=20}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- click on the other line
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    -- cursor moves
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:pos')
    end
    function test_draw_text_wrapping_within_word()
    -- arrange a screen line that needs to be split within a word
    io.write('\ntest_draw_text_wrapping_within_word')
    App.screen.init{width=60, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abcd e fghijk', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abcd ', 'F - test_draw_text_wrapping_within_word/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'e fgh', 'F - test_draw_text_wrapping_within_word/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ijk', 'F - test_draw_text_wrapping_within_word/screen:3')
    end
    function test_draw_wrapping_text_containing_non_ascii()
    -- draw a long line containing non-ASCII
    io.write('\ntest_draw_wrapping_text_containing_non_ascii')
    App.screen.init{width=60, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'madam I’m adam', 'xyz'} -- notice the non-ASCII apostrophe
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'mad', 'F - test_draw_wrapping_text_containing_non_ascii/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'am I', 'F - test_draw_wrapping_text_containing_non_ascii/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, '’m a', 'F - test_draw_wrapping_text_containing_non_ascii/screen:3')
    end
    function test_click_on_wrapping_line()
    io.write('\ntest_click_on_wrapping_line')
    -- display a wrapping line
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    -- 12345678901234
    Editor_state.lines = load_array{"madam I'm adam"}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'madam ', 'F - test_click_on_wrapping_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line/baseline/screen:2')
    y = y + Editor_state.line_height
    -- click past end of second screen line
    edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
    -- cursor moves to end of screen line
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line/cursor:pos')
    end
    function test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()
    io.write('\ntest_click_on_wrapping_line_rendered_from_partway_at_top_of_screen')
    -- display a wrapping line from its second screen line
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    -- 12345678901234
    Editor_state.lines = load_array{"madam I'm adam"}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=8}
    Editor_state.screen_top1 = {line=1, pos=7}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/baseline/screen:2')
    y = y + Editor_state.line_height
    -- click past end of second screen line
    edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
    -- cursor moves to end of screen line
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:line')
    check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:pos')
    end
    function test_click_past_end_of_wrapping_line()
    io.write('\ntest_click_past_end_of_wrapping_line')
    -- display a wrapping line
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    -- 12345678901234
    Editor_state.lines = load_array{"madam I'm adam"}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, "I'm ad", 'F - test_click_past_end_of_wrapping_line/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line/baseline/screen:3')
    y = y + Editor_state.line_height
    -- click past the end of it
    edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
    -- cursor moves to end of line
    check_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line/cursor') -- one more than the number of UTF-8 code-points
    end
    function test_click_past_end_of_wrapping_line_containing_non_ascii()
    io.write('\ntest_click_past_end_of_wrapping_line_containing_non_ascii')
    -- display a wrapping line containing non-ASCII
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    -- 12345678901234
    Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostrophe
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'I’m ad', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:3')
    y = y + Editor_state.line_height
    -- click past the end of it
    edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
    -- cursor moves to end of line
    check_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/cursor') -- one more than the number of UTF-8 code-points
    end
    function test_click_past_end_of_word_wrapping_line()
    io.write('\ntest_click_past_end_of_word_wrapping_line')
    -- display a long line wrapping at a word boundary on a screen of more realistic length
    App.screen.init{width=160, height=80}
    Editor_state = edit.initialize_test_state()
    -- 0 1 2
    -- 123456789012345678901
    Editor_state.lines = load_array{'the quick brown fox jumped over the lazy dog'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'the quick brown fox ', 'F - test_click_past_end_of_word_wrapping_line/baseline/screen:1')
    y = y + Editor_state.line_height
    -- click past the end of the screen line
    edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
    -- cursor moves to end of screen line
    check_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')
    end
    function test_edit_wrapping_text()
    io.write('\ntest_edit_wrapping_text')
    App.screen.init{width=50, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=4}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    edit.run_after_textinput(Editor_state, 'g')
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')
    end
    function test_insert_newline()
    io.write('\ntest_insert_newline')
    -- display a few lines
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')
    -- hitting the enter key splits the line
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_newline/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'a', 'F - test_insert_newline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_insert_newline/screen:3')
    end
    function test_insert_newline_at_start_of_line()
    io.write('\ntest_insert_newline_at_start_of_line')
    -- display a line
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- hitting the enter key splits the line
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos')
    check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1')
    check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
    end
    function test_insert_from_clipboard()
    io.write('\ntest_insert_from_clipboard')
    -- display a few lines
    App.screen.init{width=Editor_state.left+30, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')
    -- paste some text including a newline, check that new line is created
    App.clipboard = 'xy\nz'
    edit.run_after_keychord(Editor_state, 'C-v')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_from_clipboard/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_from_clipboard/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_insert_from_clipboard/cursor:pos')
    y = Editor_state.top
    App.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3')
    end
    function test_move_cursor_using_mouse()
    io.write('\ntest_move_cursor_using_mouse')
    App.screen.init{width=50, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'xyz'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache
    edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    check_eq(Editor_state.cursor1.line, 1, 'F - test_move_cursor_using_mouse/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_cursor_using_mouse/cursor:pos')
    end
    function test_pagedown()
    io.write('\ntest_pagedown')
    App.screen.init{width=120, height=45}
    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=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    -- initially the first two lines are displayed
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'abc', 'F - test_pagedown/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'def', 'F - test_pagedown/baseline/screen:2')
    -- after pagedown the bottom line becomes the top
    edit.run_after_keychord(Editor_state, 'pagedown')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown/screen_top')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_pagedown/cursor')
    y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_pagedown/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')
  • file deletion: source_text.lua (----------)source_text.lua (----------)
    [6.2][6.147062:147101](),[6.2][6.147062:147101](),[6.147101][6.83723:83723]()
    if State.lines[loc1.line].mode == 'drawing' then
    return {line=loc1.line, screen_line=1, screen_pos=1}
    end
    if loc1.pos then
    return Text.to2A(State, loc1)
    else
    return Text.to2B(State, loc1)
    end
    end
    function Text.to2A(State, loc1)
    local result = {line=loc1.line}
    local line_cache = State.line_cache[loc1.line]
    Text.populate_screen_line_starting_pos(State, loc1.line)
    for i=#line_cache.screen_line_starting_pos,1,-1 do
    local spos = line_cache.screen_line_starting_pos[i]
    if spos <= loc1.pos then
    result.screen_line = i
    result.screen_pos = loc1.pos - spos + 1
    break
    end
    end
    assert(result.screen_pos)
    return result
    end
    function Text.to2B(State, loc1)
    local result = {line=loc1.line}
    local line_cache = State.line_cache[loc1.line]
    Text.populate_screen_line_starting_pos(State, loc1.line)
    local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
    Text.populate_screen_line_starting_posB(State, loc1.line, x)
    for i=#line_cache.screen_line_starting_posB,1,-1 do
    local sposB = line_cache.screen_line_starting_posB[i]
    if sposB <= loc1.posB then
    result.screen_lineB = i
    result.screen_posB = loc1.posB - sposB + 1
    break
    end
    end
    assert(result.screen_posB)
    return result
    end
    function Text.to1(State, loc2)
    if loc2.screen_pos then
    return Text.to1A(State, loc2)
    else
    return Text.to1B(State, loc2)
    end
    end
    function Text.to1A(State, loc2)
    local result = {line=loc2.line, pos=loc2.screen_pos}
    if loc2.screen_line > 1 then
    result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
    end
    return result
    end
    function Text.to1B(State, loc2)
    local result = {line=loc2.line, posB=loc2.screen_posB}
    if loc2.screen_lineB > 1 then
    result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1
    end
    return result
    end
    function Text.lt1(a, b)
    if a.line < b.line then
    return true
    end
    if a.line > b.line then
    return false
    end
    -- A side < B side
    if a.pos and not b.pos then
    return true
    end
    if not a.pos and b.pos then
    return false
    end
    if a.pos then
    return a.pos < b.pos
    else
    return a.posB < b.posB
    end
    end
    function Text.le1(a, b)
    return eq(a, b) or Text.lt1(a, b)
    end
    function Text.offset(s, pos1)
    if pos1 == 1 then return 1 end
    local result = utf8.offset(s, pos1)
    if result == nil then
    print(pos1, #s, s)
    end
    assert(result)
    return result
    end
    function Text.previous_screen_line(State, loc2)
    if loc2.screen_pos then
    return Text.previous_screen_lineA(State, loc2)
    else
    return Text.previous_screen_lineB(State, loc2)
    end
    end
    function Text.previous_screen_lineA(State, loc2)
    if loc2.screen_line > 1 then
    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)
    assert(State.lines[top2.line-1].mode == 'drawing')
    -- 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
    end
    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
    -- 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)
    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
    --? print('scroll up')
    Text.snap_cursor_to_bottom_of_screen(State)
    end
    end
    -- should never modify State.cursor1
    function Text.snap_cursor_to_bottom_of_screen(State)
    --? print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
    local top2 = Text.to2(State, State.cursor1)
    --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
    -- slide to start of screen line
    if top2.screen_pos then
    top2.screen_pos = 1
    else
    assert(top2.screen_posB)
    top2.screen_posB = 1
    end
    --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
    --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
    local y = App.screen.height - State.line_height
    -- duplicate some logic from love.draw
    while true do
    --? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
    if top2.line == 1 and top2.screen_line == 1 then break end
    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
    end
    end
    function Text.pos_at_start_of_screen_line(State, loc1)
    Text.populate_screen_line_starting_pos(State, loc1.line)
    local line_cache = State.line_cache[loc1.line]
    for i=#line_cache.screen_line_starting_pos,1,-1 do
    local spos = line_cache.screen_line_starting_pos[i]
    if spos <= loc1.pos then
    return spos,i
    end
    end
    assert(false)
    end
    function Text.pos_at_start_of_screen_lineB(State, loc1)
    Text.populate_screen_line_starting_pos(State, loc1.line)
    local line_cache = State.line_cache[loc1.line]
    local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
    Text.populate_screen_line_starting_posB(State, loc1.line, x)
    for i=#line_cache.screen_line_starting_posB,1,-1 do
    local sposB = line_cache.screen_line_starting_posB[i]
    if sposB <= loc1.posB then
    return sposB,i
    end
    end
    assert(false)
    end
    function Text.cursor_at_final_screen_line(State)
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local line = State.lines[State.cursor1.line]
    local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos
    --? print(screen_lines[#screen_lines], State.cursor1.pos)
    if (not State.expanded and not line.expanded) or
    line.dataB == nil then
    return screen_lines[#screen_lines] <= State.cursor1.pos
    end
    if State.cursor1.pos then
    -- ignore B side
    return screen_lines[#screen_lines] <= State.cursor1.pos
    end
    assert(State.cursor1.posB)
    local line_cache = State.line_cache[State.cursor1.line]
    local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
    Text.populate_screen_line_starting_posB(State, State.cursor1.line, x)
    local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB
    return screen_lines[#screen_lines] <= State.cursor1.posB
    end
    function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
    else
    -- overflow back into A side
    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
    end
    end
    function Text.right_without_scrollB(State)
    if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
    State.cursor1.posB = State.cursor1.posB+1
    assert(State.lines[State.cursor1.line].mode == 'text')
    if State.cursor1.pos then
    Text.right_without_scrollA(State)
    else
    Text.right_without_scrollB(State)
    end
    end
    function Text.right_without_scrollA(State)
    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 > 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
    end
    if Text.lt1(State.cursor1, State.screen_top1) then
    local top2 = Text.to2(State, State.screen_top1)
    top2 = Text.previous_screen_line(State, top2)
    State.screen_top1 = Text.to1(State, top2)
    end
    end
    function Text.leftB(State)
    if State.cursor1.posB > 1 then
    State.cursor1.posB = State.cursor1.posB-1
    else
    -- overflow back into A side
    State.cursor1.posB = nil
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
    end
    if Text.lt1(State.cursor1, State.screen_top1) then
    local top2 = Text.to2(State, State.screen_top1)
    top2 = Text.previous_screen_line(State, top2)
    State.screen_top1 = Text.to1(State, top2)
    end
    end
    function Text.right(State)
    Text.right_without_scroll(State)
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State)
    end
    end
    function Text.right_without_scroll(State)
    State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copy
    end
    end
    function Text.end_of_line(State)
    if State.cursor1.pos then
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
    else
    State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1
    end
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State)
    end
    end
    function Text.word_left(State)
    -- we can cross the fold, so check side A/B one level down
    Text.skip_whitespace_left(State)
    Text.left(State)
    Text.skip_non_whitespace_left(State)
    end
    function Text.word_right(State)
    -- we can cross the fold, so check side A/B one level down
    Text.skip_whitespace_right(State)
    Text.right(State)
    Text.skip_non_whitespace_right(State)
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State)
    end
    end
    function Text.skip_whitespace_left(State)
    if State.cursor1.pos then
    Text.skip_whitespace_leftA(State)
    else
    Text.skip_whitespace_leftB(State)
    end
    end
    function Text.skip_non_whitespace_left(State)
    if State.cursor1.pos then
    Text.skip_non_whitespace_leftA(State)
    else
    Text.skip_non_whitespace_leftB(State)
    end
    end
    function Text.skip_whitespace_leftA(State)
    while true do
    if State.cursor1.pos == 1 then
    break
    end
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
    break
    end
    Text.left(State)
    end
    end
    function Text.skip_whitespace_leftB(State)
    while true do
    if State.cursor1.posB == 1 then
    break
    end
    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') then
    break
    end
    Text.left(State)
    end
    end
    function Text.skip_non_whitespace_leftA(State)
    while true do
    if State.cursor1.pos == 1 then
    break
    end
    assert(State.cursor1.pos > 1)
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
    break
    end
    Text.left(State)
    end
    end
    function Text.skip_non_whitespace_leftB(State)
    while true do
    if State.cursor1.posB == 1 then
    break
    end
    assert(State.cursor1.posB > 1)
    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then
    break
    end
    Text.left(State)
    end
    end
    function Text.skip_whitespace_right(State)
    if State.cursor1.pos then
    Text.skip_whitespace_rightA(State)
    else
    Text.skip_whitespace_rightB(State)
    end
    end
    function Text.skip_non_whitespace_right(State)
    if State.cursor1.pos then
    Text.skip_non_whitespace_rightA(State)
    else
    Text.skip_non_whitespace_rightB(State)
    end
    end
    function Text.skip_whitespace_rightA(State)
    while true do
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
    break
    end
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
    break
    end
    Text.right_without_scroll(State)
    end
    end
    function Text.skip_whitespace_rightB(State)
    while true do
    if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
    break
    end
    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') then
    break
    end
    Text.right_without_scroll(State)
    end
    end
    function Text.skip_non_whitespace_rightA(State)
    while true do
    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
    break
    end
    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
    break
    end
    Text.right_without_scroll(State)
    end
    end
    function Text.skip_non_whitespace_rightB(State)
    while true do
    if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
    break
    end
    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then
    break
    end
    Text.right_without_scroll(State)
    end
    end
    function Text.match(s, pos, pat)
    local start_offset = Text.offset(s, pos)
    assert(start_offset)
    local end_offset = Text.offset(s, pos+1)
    assert(end_offset > start_offset)
    local curr = s:sub(start_offset, end_offset-1)
    return curr:match(pat)
    end
    function Text.left(State)
    if State.cursor1.pos then
    Text.leftA(State)
    else
    Text.leftB(State)
    end
    end
    function Text.leftA(State)
    if State.cursor1.pos > 1 then
    State.cursor1.pos = State.cursor1.pos-1
    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.lines[new_cursor_line].data, State.cursor_x, State.left),
    }
    --? print(State.cursor1.pos)
    break
    end
    end
    if State.cursor1.line > State.screen_bottom1.line then
    --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
    --? print('scroll up preserving cursor')
    Text.snap_cursor_to_bottom_of_screen(State)
    --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
    end
    elseif State.cursor1.pos then
    -- move down one screen line (A side) in current line
    local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
    --? print('cursor is NOT at final screen line of its line')
    local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]
    --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
    State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
    if scroll_down then
    --? print('scroll up preserving cursor')
    Text.snap_cursor_to_bottom_of_screen(State)
    --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
    end
    else
    -- move down one screen line (B side) in current line
    local scroll_down = false
    if Text.le1(State.screen_bottom1, State.cursor1) then
    scroll_down = true
    end
    local cursor_line = State.lines[State.cursor1.line]
    local cursor_line_cache = State.line_cache[State.cursor1.line]
    local cursor2 = Text.to2(State, State.cursor1)
    assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB)
    local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
    Text.populate_screen_line_starting_posB(State, State.cursor1.line)
    local new_screen_line_starting_posB = cursor_line_cache.screen_line_starting_posB[screen_line_indexB+1]
    local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB)
    local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB)
    State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    if scroll_down then
    Text.snap_cursor_to_bottom_of_screen(State)
    end
    end
    --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
    end
    function Text.start_of_line(State)
    if State.cursor1.pos then
    State.cursor1.pos = 1
    else
    State.cursor1.posB = 1
    end
    if Text.lt1(State.cursor1, State.screen_top1) then
    assert(State.lines[State.cursor1.line].mode == 'text')
    --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.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 > 1 do
    new_cursor_line = new_cursor_line-1
    if State.lines[new_cursor_line].mode == 'text' then
    State.cursor1 = {line=State.cursor1.line-1, posB=nil}
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local prev_line_cache = State.line_cache[State.cursor1.line]
    local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
    local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
    State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    break
    end
    end
    elseif screen_line_indexB == 2 then
    -- all-B screen-line to potentially A+B screen-line
    local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
    if State.cursor_x < xA then
    State.cursor1.posB = nil
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
    State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    else
    Text.populate_screen_line_starting_posB(State, State.cursor1.line)
    local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
    local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
    local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
    State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1
    end
    else
    assert(screen_line_indexB > 2)
    -- all-B screen-line to all-B screen-line
    Text.populate_screen_line_starting_posB(State, State.cursor1.line)
    local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
    local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
    local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
    State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    end
    if Text.lt1(State.cursor1, State.screen_top1) then
    local top2 = Text.to2(State, State.screen_top1)
    top2 = Text.previous_screen_line(State, top2)
    State.screen_top1 = Text.to1(State, top2)
    end
    end
    -- cursor on final screen line (A or B side) => goes to next screen line on A side
    -- cursor on A side => move down one screen line (A side) in current line
    -- cursor on B side => move down one screen line (B side) in current line
    function Text.down(State)
    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=State.cursor1.line-1, 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(s, State.cursor_x, State.left) - 1
    break
    end
    end
    else
    -- move up one screen line in current line
    assert(screen_line_index > 1)
    local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
    State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    --? print('cursor pos is now '..tostring(State.cursor1.pos))
    end
    if Text.lt1(State.cursor1, State.screen_top1) then
    local top2 = Text.to2(State, State.screen_top1)
    top2 = Text.previous_screen_line(State, top2)
    State.screen_top1 = Text.to1(State, top2)
    end
    end
    function Text.upB(State)
    local line_cache = State.line_cache[State.cursor1.line]
    local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
    assert(screen_line_indexB >= 1)
    if screen_line_indexB == 1 then
    -- move to A side of previous line
    assert(State.lines[State.cursor1.line].mode == 'text')
    if State.cursor1.pos then
    Text.upA(State)
    else
    Text.upB(State)
    end
    end
    function Text.upA(State)
    --? 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
    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB}
    end
    --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}
    Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
    --? print('top now', State.screen_top1.line)
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    --? print('pagedown end')
    end
    function Text.up(State)
    if State.lines[State.screen_top1.line].mode == 'text' then
    y = y - State.line_height
    elseif State.lines[State.screen_top1.line].mode == 'drawing' then
    y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
    end
    top2 = Text.previous_screen_line(State, top2)
    end
    State.screen_top1 = Text.to1(State, top2)
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}
    Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
    --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
    --? print('pageup end')
    end
    function Text.pagedown(State)
    --? print('pagedown')
    local bot2 = Text.to2(State, State.screen_bottom1)
    local new_top1 = Text.to1(State, bot2)
    if Text.lt1(State.screen_top1, new_top1) then
    State.screen_top1 = new_top1
    else
    table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
    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)
    State.lines[State.cursor1.line].dataB = nil
    Text.clear_screen_line_cache(State, State.cursor1.line)
    State.cursor1 = {line=State.cursor1.line+1, pos=1}
    else
    -- disable enter when cursor is on the B side
    end
    end
    function Text.pageup(State)
    --? print('pageup')
    -- duplicate some logic from love.draw
    local top2 = Text.to2(State, State.screen_top1)
    --? print(App.screen.height)
    local y = App.screen.height - State.line_height
    while y >= State.top do
    --? print(y, top2.line, top2.screen_line, top2.screen_pos)
    if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break end
    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
    -- delete side B on first line
    State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
    end
    table.remove(State.lines, State.cursor1.line+1)
    table.remove(State.line_cache, State.cursor1.line+1)
    end
    Text.clear_screen_line_cache(State, State.cursor1.line)
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
    --== shortcuts that move the cursor
    elseif chord == 'left' then
    Text.left(State)
    elseif chord == 'right' then
    Text.right(State)
    elseif chord == 'S-left' then
    Text.left(State)
    elseif chord == 'S-right' then
    Text.right(State)
    -- C- hotkeys reserved for drawings, so we'll use M-
    elseif chord == 'M-left' then
    Text.word_left(State)
    elseif chord == 'M-right' then
    Text.word_right(State)
    elseif chord == 'M-S-left' then
    Text.word_left(State)
    elseif chord == 'M-S-right' then
    Text.word_right(State)
    elseif chord == 'home' then
    Text.start_of_line(State)
    elseif chord == 'end' then
    Text.end_of_line(State)
    elseif chord == 'S-home' then
    Text.start_of_line(State)
    elseif chord == 'S-end' then
    Text.end_of_line(State)
    elseif chord == 'up' then
    Text.up(State)
    elseif chord == 'down' then
    Text.down(State)
    elseif chord == 'S-up' then
    Text.up(State)
    elseif chord == 'S-down' then
    Text.down(State)
    elseif chord == 'pageup' then
    Text.pageup(State)
    elseif chord == 'pagedown' then
    Text.pagedown(State)
    elseif chord == 'S-pageup' then
    Text.pageup(State)
    elseif chord == 'S-pagedown' then
    Text.pagedown(State)
    end
    end
    function Text.insert_return(State)
    if State.cursor1.pos then
    -- when inserting a newline, move any B side to the new line
    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    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
    State.cursor1.line = State.cursor1.line-1
    end
    if State.screen_top1.line > #State.lines then
    Text.populate_screen_line_starting_pos(State, #State.lines)
    local line_cache = State.line_cache[#State.line_cache]
    State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
    elseif Text.lt1(State.cursor1, State.screen_top1) then
    local top2 = Text.to2(State, State.screen_top1)
    top2 = Text.previous_screen_line(State, top2, State.left, State.right)
    State.screen_top1 = Text.to1(State, top2)
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    end
    Text.clear_screen_line_cache(State, State.cursor1.line)
    assert(Text.le1(State.screen_top1, State.cursor1))
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
    elseif chord == 'delete' then
    local before
    if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
    before = snapshot(State, State.cursor1.line)
    else
    before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
    end
    if State.cursor1.pos and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
    local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
    if byte_start then
    if byte_end then
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
    else
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
    end
    -- no change to State.cursor1.pos
    end
    elseif State.cursor1.posB then
    if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
    local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)
    if byte_start then
    if byte_end then
    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
    else
    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
    end
    -- no change to State.cursor1.pos
    end
    else
    -- refuse to delete past end of side B
    end
    elseif State.cursor1.line < #State.lines then
    if line.mode ~= 'text' then return end
    local line_cache = State.line_cache[line_index]
    if line_cache.fragments then
    return
    end
    line_cache.fragments = {}
    local x = State.left
    -- try to wrap at word boundaries
    for frag in line.data:gmatch('%S*%s*') do
    local frag_text = App.newText(love.graphics.getFont(), frag)
    local frag_width = App.width(frag_text)
    --? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')
    while x + frag_width > State.right do
    --? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
    if (x-State.left) < 0.8 * (State.right-State.left) then
    --? print('splitting')
    -- long word; chop it at some letter
    -- We're not going to reimplement TeX here.
    local bpos = Text.nearest_pos_less_than(frag, State.right - x)
    --? print('bpos', bpos)
    if bpos == 0 then break end -- avoid infinite loop when window is too narrow
    local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
    --? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
    local frag1 = string.sub(frag, 1, boffset-1)
    local frag1_text = App.newText(love.graphics.getFont(), frag1)
    local frag1_width = App.width(frag1_text)
    --? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
    assert(x + frag1_width <= State.right)
    table.insert(line_cache.fragments, {data=frag1, text=frag1_text})
    frag = string.sub(frag, boffset)
    frag_text = App.newText(love.graphics.getFont(), frag)
    frag_width = App.width(frag_text)
    end
    x = State.left -- new line
    end
    if #frag > 0 then
    --? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
    table.insert(line_cache.fragments, {data=frag, text=frag_text})
    end
    x = x + frag_width
    end
    end
    function Text.populate_screen_line_starting_posB(State, line_index, x)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    if line_cache.screen_line_starting_posB then
    return
    end
    -- duplicate some logic from Text.draw
    Text.compute_fragmentsB(State, line_index, x)
    line_cache.screen_line_starting_posB = {1}
    local pos = 1
    for _, f in ipairs(line_cache.fragmentsB) do
    local frag, frag_text = f.data, f.text
    -- render fragment
    local frag_width = App.width(frag_text)
    if x + frag_width > State.right then
    x = State.left
    table.insert(line_cache.screen_line_starting_posB, pos)
    end
    x = x + frag_width
    local frag_len = utf8.len(frag)
    pos = pos + frag_len
    end
    end
    function Text.compute_fragmentsB(State, line_index, x)
    --? print('compute_fragmentsB', line_index, 'between', x, State.right)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    if line_cache.fragmentsB then
    return
    end
    line_cache.fragmentsB = {}
    -- try to wrap at word boundaries
    for frag in line.dataB:gmatch('%S*%s*') do
    local frag_text = App.newText(love.graphics.getFont(), frag)
    local frag_width = App.width(frag_text)
    --? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
    while x + frag_width > State.right do
    --? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
    if (x-State.left) < 0.8 * (State.right-State.left) then
    --? print('splitting')
    -- long word; chop it at some letter
    -- We're not going to reimplement TeX here.
    local bpos = Text.nearest_pos_less_than(frag, State.right - x)
    --? print('bpos', bpos)
    if bpos == 0 then break end -- avoid infinite loop when window is too narrow
    local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
    --? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
    local frag1 = string.sub(frag, 1, boffset-1)
    local frag1_text = App.newText(love.graphics.getFont(), frag1)
    local frag1_width = App.width(frag1_text)
    --? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
    assert(x + frag1_width <= State.right)
    table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})
    frag = string.sub(frag, boffset)
    frag_text = App.newText(love.graphics.getFont(), frag)
    frag_width = App.width(frag_text)
    end
    x = State.left -- new line
    end
    if #frag > 0 then
    --? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
    table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})
    end
    x = x + frag_width
    end
    end
    function Text.textinput(State, t)
    if App.mouse_down(1) then return end
    if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end
    local before = snapshot(State, State.cursor1.line)
    --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
    Text.insert_at_cursor(State, t)
    if State.cursor_y > App.screen.height - State.line_height then
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    end
    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
    end
    function Text.insert_at_cursor(State, t)
    if State.cursor1.pos then
    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
    Text.clear_screen_line_cache(State, State.cursor1.line)
    State.cursor1.pos = State.cursor1.pos+1
    else
    assert(State.cursor1.posB)
    local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)
    Text.clear_screen_line_cache(State, State.cursor1.line)
    State.cursor1.posB = State.cursor1.posB+1
    end
    end
    -- Don't handle any keys here that would trigger love.textinput above.
    function Text.keychord_pressed(State, chord)
    --? print('chord', chord)
    --== shortcuts that mutate text
    if chord == 'return' then
    local before_line = State.cursor1.line
    local before = snapshot(State, before_line)
    Text.insert_return(State)
    if State.cursor_y > App.screen.height - State.line_height then
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    end
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    elseif chord == 'tab' then
    local before = snapshot(State, State.cursor1.line)
    --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
    Text.insert_at_cursor(State, '\t')
    if State.cursor_y > App.screen.height - State.line_height then
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
    end
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
    elseif chord == 'backspace' then
    local before
    if State.cursor1.pos and State.cursor1.pos > 1 then
    before = snapshot(State, State.cursor1.line)
    local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
    if byte_start then
    if byte_end then
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
    else
    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
    end
    State.cursor1.pos = State.cursor1.pos-1
    end
    elseif State.cursor1.posB then
    if State.cursor1.posB > 1 then
    before = snapshot(State, State.cursor1.line)
    local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)
    local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
    if byte_start then
    if byte_end then
    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
    else
    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
    end
    State.cursor1.posB = State.cursor1.posB-1
    end
    else
    -- refuse to delete past beginning of side B
    end
    elseif State.cursor1.line > 1 then
    before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
    if line.mode ~= 'text' then return end
    local line_cache = State.line_cache[line_index]
    if line_cache.screen_line_starting_pos then
    return
    end
    -- duplicate some logic from Text.draw
    Text.compute_fragments(State, line_index)
    line_cache.screen_line_starting_pos = {1}
    local x = State.left
    local pos = 1
    for _, f in ipairs(line_cache.fragments) do
    local frag, frag_text = f.data, f.text
    -- render fragment
    local frag_width = App.width(frag_text)
    if x + frag_width > State.right then
    x = State.left
    table.insert(line_cache.screen_line_starting_pos, pos)
    end
    x = x + frag_width
    local frag_len = utf8.len(frag)
    pos = pos + frag_len
    end
    end
    function Text.compute_fragments(State, line_index)
    --? print('compute_fragments', line_index, 'between', State.left, State.right)
    local line = State.lines[line_index]
  • file deletion: source_file.lua (----------)source_file.lua (----------)
    [6.2][6.152399:152438](),[6.2][6.152399:152438](),[6.152438][6.150241:150241]()
    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)
    --? 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
    print(shape.mode)
    assert(false)
    end
    table.insert(drawing.shapes, shape)
    end
    return i, drawing
    end
    table.insert(result, {mode='text', data=''})
    end
    return result
    end
    --? print('inserting text')
    local line_info = {mode='text'}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    else
    line_info.data = line
    end
    table.insert(result, line_info)
    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
    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)
    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
    print(shape.mode)
    assert(false)
    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), '\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, '\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, '\n')
    elseif shape.mode == 'circle' then
    outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\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}), '\n')
    elseif shape.mode == 'deleted' then
    -- ignore
    else
    print(shape.mode)
    assert(false)
    end
    end
    outfile:write('```\n')
    end
    -- for tests
    function load_array(a)
    local result = {}
    local next_line = ipairs(a)
    local i,line,drawing = 0, ''
    while true do
    i,line = next_line(a, i)
    if i == nil then break end
    if line.mode == 'drawing' then
    store_drawing(outfile, line)
    else
    outfile:write(line.data)
    if line.dataB and #line.dataB > 0 then
    outfile:write(Fold)
    outfile:write(line.dataB)
    end
    outfile:write('\n')
    end
    table.insert(result, {mode='text', data=''})
    end
    return result
    end
    function save_to_disk(State)
    local outfile = App.open_for_writing(State.filename)
    if outfile == nil then
    error('failed to write to "'..State.filename..'"')
    end
    for _,line in ipairs(State.lines) do
    local line_info = {mode='text'}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    else
    line_info.data = line
    end
    table.insert(result, line_info)
    end
    if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated
    table.insert(result, load_drawing(infile_next_line))
    else
  • file deletion: source_edit.lua (----------)source_edit.lua (----------)
    [6.2][6.165725:165764](),[6.2][6.165725:165764](),[6.165764][6.152440:152440]()
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line_cache.starty here
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index then
    local before = snapshot(State, drawing_index)
    Drawing.keychord_pressed(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.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)
    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
    schedule_save(State)
    else
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    Text.keychord_pressed(State, chord)
    end
    end
    function edit.eradicate_locations_after_the_fold(State)
    -- eradicate side B from any locations we track
    if State.cursor1.posB then
    State.cursor1.posB = nil
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
    State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
    end
    if State.screen_top1.posB then
    State.screen_top1.posB = nil
    State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
    State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
    end
    end
    function edit.key_released(State, key, scancode)
    end
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    love.graphics.setFont(love.graphics.newFont(Editor_state.font_height))
    State.line_height = math.floor(font_height*1.3)
    State.em = App.newText(love.graphics.getFont(), 'm')
    Text_cache = {}
    end
    --== some methods for tests
    Test_margin_left = 25
    function edit.initialize_test_state()
    -- if you change these values, tests will start failing
    return edit.initialize_state(
    15, -- top margin
    Test_margin_left,
    App.screen.width, -- right margin = 0
    14, -- font height assuming default LÖVE font
    15) -- line height
    end
    -- all textinput events are also keypresses
    -- TODO: handle chords of multiple keys
    function edit.run_after_textinput(State, t)
    edit.keychord_pressed(State, t)
    edit.textinput(State, t)
    edit.key_released(State, t)
    App.screen.contents = {}
    edit.draw(State)
    end
    -- not all keys are textinput
    function edit.run_after_keychord(State, chord)
    edit.keychord_pressed(State, chord)
    edit.key_released(State, chord)
    App.screen.contents = {}
    edit.draw(State)
    end
    function edit.run_after_mouse_click(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.mouse_pressed(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_released(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.draw(State)
    end
    function edit.run_after_mouse_press(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.mouse_pressed(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.draw(State)
    end
    function edit.run_after_mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_released(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.draw(State)
    end
    -- 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)
    schedule_save(State)
    end
    -- clipboard
    elseif chord == 'C-c' then
    local s = Text.selection(State)
    if s then
    App.setClipboardText(s)
    end
    elseif chord == 'C-x' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local s = Text.cut_selection(State, State.left, State.right)
    if s then
    App.setClipboardText(s)
    end
    schedule_save(State)
    elseif chord == 'C-v' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    -- We don't have a good sense of when to scroll, so we'll be conservative
    -- and sometimes scroll when we didn't quite need to.
    local before_line = State.cursor1.line
    local before = snapshot(State, before_line)
    local clipboard_data = App.getClipboardText()
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    Text.insert_return(State)
    else
    Text.insert_at_cursor(State, c)
    end
    end
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    end
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    -- 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)
    schedule_save(State)
    end
    elseif chord == 'C-y' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = redo_event(State)
    if event then
    local src = event.after
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    patch(State.lines, event.before, event.after)
    elseif chord == 'C-i' then
    if State.cursor1.posB == nil then
    local before = snapshot(State, State.cursor1.line)
    if State.lines[State.cursor1.line].dataB == nil then
    State.lines[State.cursor1.line].dataB = ''
    end
    State.lines[State.cursor1.line].expanded = true
    State.cursor1.pos = nil
    State.cursor1.posB = 1
    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, State.cursor1.line)})
    end
    -- zoom
    elseif chord == 'C-=' then
    edit.update_font_settings(State, State.font_height+2)
    Text.redraw_all(State)
    elseif chord == 'C--' then
    edit.update_font_settings(State, State.font_height-2)
    Text.redraw_all(State)
    elseif chord == 'C-0' then
    edit.update_font_settings(State, 20)
    Text.redraw_all(State)
    -- undo
    elseif chord == 'C-z' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = undo_event(State)
    if event then
    local src = event.before
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    patch(State.lines, event.after, event.before)
    patch_placeholders(State.line_cache, event.after, event.before)
    elseif 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
    Text.textinput(State, t)
    end
    schedule_save(State)
    end
    function edit.keychord_pressed(State, chord, key)
    if State.search_term then
    if chord == 'escape' then
    State.search_term = nil
    State.search_text = nil
    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
    elseif chord == 'return' then
    State.search_term = nil
    State.search_text = nil
    State.search_backup = nil
    elseif chord == 'backspace' then
    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)
    State.search_text = nil
    elseif chord == 'down' then
    if State.cursor1.pos then
    State.cursor1.pos = State.cursor1.pos+1
    else
    State.cursor1.posB = State.cursor1.posB+1
    end
    Text.search_next(State)
    elseif chord == 'up' then
    Text.search_previous(State)
    end
    return
    elseif chord == 'C-f' then
    State.search_term = ''
    State.search_backup = {
    cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},
    screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},
    }
    assert(State.search_text == nil)
    -- bifold text
    elseif chord == 'C-b' then
    State.expanded = not State.expanded
    Text.redraw_all(State)
    if not State.expanded then
    for _,line in ipairs(State.lines) do
    line.expanded = nil
    end
    edit.eradicate_locations_after_the_fold(State)
    end
    if State.search_term then return end
    --? print('release')
    if State.lines.current_drawing then
    Drawing.mouse_released(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
    end
    end
    function edit.textinput(State, t)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    if State.search_term then
    State.search_term = State.search_term..t
    State.search_text = nil
    Text.search_next(State)
    if line.mode == 'text' then
    if Text.in_line(State, line_index, x,y) then
    local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
    --? print(x,y, 'setting cursor:', line_index, pos, posB)
    State.cursor1 = {line=line_index, pos=pos, posB=posB}
    break
    end
    elseif line.mode == 'drawing' then
    local line_cache = State.line_cache[line_index]
    if Drawing.in_drawing(line, line_cache, 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_pressed(State, line_index, x,y, mouse_button)
    break
    end
    end
    end
    end
    function edit.mouse_released(State, x,y, mouse_button)
    --? print('press')
    if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
    -- press on a button and it returned 'true' to short-circuit
    return
    end
    for line_index,line in ipairs(State.lines) do
    Drawing.update(State, dt)
    if State.next_save and State.next_save < App.getTime() then
    save_to_disk(State)
    State.next_save = nil
    end
    end
    function schedule_save(State)
    if State.next_save == nil then
    State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
    end
    end
    function edit.quit(State)
    -- make sure to save before quitting
    if State.next_save then
    save_to_disk(State)
    end
    end
    function edit.mouse_pressed(State, x,y, mouse_button)
    if State.search_term then return end
    y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)
    y = y + State.line_height
    --? 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
    print(line.mode)
    assert(false)
    end
    local startpos, startposB = 1, nil
    if line_index == State.screen_top1.line then
    if State.screen_top1.pos then
    startpos = State.screen_top1.pos
    else
    startpos, startposB = nil, State.screen_top1.posB
    end
    end
    if line.data == '' then
    -- button to insert new drawing
    button(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,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
    if line.mode == 'text' then
    --? print('text.draw', y, line_index)
    --? print('draw:', y, line_index, line, line.mode)
    if y + State.line_height > App.screen.height then break end
    State.screen_bottom1 = {line=line_index, pos=nil, posB=nil}
    function edit.fixup_cursor(State)
    for i,line in ipairs(State.lines) do
    if line.mode == 'text' then
    State.cursor1.line = i
    break
    end
    end
    end
    function edit.draw(State)
    State.button_handlers = {}
    App.color(Text_color)
    assert(#State.lines == #State.line_cache)
    if not Text.le1(State.screen_top1, State.cursor1) then
    print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
    assert(false)
    end
    State.cursor_x = nil
    State.cursor_y = nil
    local y = State.top
    --? print('== draw')
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    current_drawing_mode = 'line',
    previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points
    font_height = font_height,
    line_height = line_height,
    em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width
    top = top,
    left = left,
    right = right,
    width = right-left,
    filename = love.filesystem.getUserDirectory()..'/lines.txt',
    next_save = nil,
    -- undo
    history = {},
    next_history = 1,
    -- search
    search_term = nil,
    search_text = nil,
    search_backup = nil, -- stuff to restore when cancelling search
    }
    return result
    end -- App.initialize_state
    -- a line is either bifold text or a drawing
    -- a line of bifold text consists of an A side and an optional B side
    -- mode = 'text',
    -- string data,
    -- string dataB,
    -- expanded: whether to show B side
    -- a drawing is a table with:
    -- mode = 'drawing'
    -- a (y) coord in pixels (updated while painting screen),
    -- 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='', dataB=nil, expanded=nil}}, -- array of lines
    -- Lines can be too long to fit on screen, in which case they _wrap_ into
    -- multiple _screen lines_.
    -- 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
    -- starty, the y coord in pixels the line starts rendering from
    -- fragments: snippets of rendered love.graphics.Text, guaranteed to not straddle screen lines
    -- screen_line_starting_pos: optional array of grapheme 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:
    -- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
    -- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
    -- Positions (and screen line indexes) can be in either the A or the B side.
    --
    -- Most of the time we'll only persist positions in schema 1, translating to
    -- schema 2 when that's convenient.
    --
    -- Make sure these coordinates are never aliased, so that changing one causes
    -- action at a distance.
    screen_top1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at top of screen
    cursor1 = {line=1, pos=1, posB=nil}, -- position of cursor
    screen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen
    -- cursor coordinates in pixels
    cursor_x = 0,
    cursor_y = 0,
    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_height, line_height) -- currently always draws to bottom of screen
    local result = {
    Drawing_padding_top = 10
    Drawing_padding_bottom = 10
    Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
    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}
    Fold_color = {r=0, g=0.6, b=0}
    Fold_background_color = {r=0, g=0.7, b=0}
    Margin_top = 15
    Margin_left = 25
    Margin_right = 25
    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
  • file deletion: source.lua (----------)source.lua (----------)
    [6.2][6.177652:177686](),[6.2][6.177652:177686](),[6.177686][6.165766:165766]()
    'geom',
    'drawing_tests',
    if x < Editor_state.right + Margin_right then
    --? print('click on edit side')
    if Focus ~= 'edit' then
    Focus = 'edit'
    end
    edit.mouse_pressed(Editor_state, x,y, mouse_button)
    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'
    end
    log_browser.mouse_pressed(Log_browser_state, x,y, mouse_button)
    for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scroll
    end
    end
    function source.mouse_released(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    if Focus == 'edit' then
    return edit.mouse_released(Editor_state, x,y, mouse_button)
    else
    return log_browser.mouse_released(Log_browser_state, x,y, mouse_button)
    end
    end
    function source.textinput(t)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    if Focus == 'edit' then
    return edit.textinput(Editor_state, t)
    else
    return log_browser.textinput(Log_browser_state, t)
    end
    end
    function source.keychord_pressed(chord, key)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    --? print('source keychord')
    if Show_file_navigator then
    keychord_pressed_on_file_navigator(chord, key)
    return
    end
    if chord == 'C-l' then
    --? print('C-l')
    Show_log_browser_side = not Show_log_browser_side
    if Show_log_browser_side then
    App.screen.width = Log_browser_state.right + Margin_right
    else
    App.screen.width = Editor_state.right + Margin_right
    end
    --? print('setting window:', App.screen.width, App.screen.height)
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    --? print('done setting window')
    -- try to restore position if possible
    -- if the window gets wider the window manager may not respect this
    source.set_window_position_from_settings(Settings.source)
    return
    end
    if chord == 'C-g' then
    Show_file_navigator = true
    File_navigation.index = 1
    return
    end
    if Focus == 'edit' then
    return edit.keychord_pressed(Editor_state, chord, key)
    else
    return log_browser.keychord_pressed(Log_browser_state, chord, key)
    end
    end
    function source.key_released(key, scancode)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    if Focus == 'edit' then
    return edit.key_released(Editor_state, key, scancode)
    else
    return log_browser.keychord_pressed(Log_browser_state, chordkey, scancode)
    end
    end
    -- use this sparingly
    function to_text(s)
    if Text_cache[s] == nil then
    Text_cache[s] = App.newText(love.graphics.getFont(), s)
    end
    return Text_cache[s]
    end
    'file',
    'source',
    'source_tests',
    'commands',
    'log_browser',
    'source_edit',
    'source_text',
    'source_undo',
    'colorize',
    'source_text_tests',
    'source_file',
    'main',
    'button',
    'keychord',
    'app',
    'test',
    'json',
    },
    index = 1,
    }
    Menu_status_bar_height = nil -- initialized below
    -- a few text objects we can avoid recomputing unless the font changes
    Text_cache = {}
    -- blinking cursor
    Cursor_time = 0
    end
    -- called only for real run
    function source.initialize()
    love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
    love.keyboard.setKeyRepeat(true)
    love.graphics.setBackgroundColor(1,1,1)
    if Settings and Settings.source then
    source.load_settings()
    else
    source.initialize_default_settings()
    end
    source.initialize_edit_side{'run.lua'}
    source.initialize_log_browser_side()
    Menu_status_bar_height = 5 + Editor_state.line_height + 5
    Editor_state.top = Editor_state.top + Menu_status_bar_height
    Log_browser_state.top = Log_browser_state.top + Menu_status_bar_height
    'drawing',
    'help',
    'text',
    'search',
    'select',
    'undo',
    'text_tests',
  • file deletion: commands.lua (----------)commands.lua (----------)
    [6.2][6.207726:207762](),[6.2][6.207726:207762](),[6.207762][6.204370:204370]()
    add_hotkey_to_menu('ctrl+i: create/edit debug print')
    add_hotkey_to_menu('ctrl+f: find in file')
    add_hotkey_to_menu('alt+left alt+right: prev/next word')
    elseif Focus == 'log_browser' then
    -- nothing yet
    else
    assert(false, 'unknown focus "'..Focus..'"')
    end
    add_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')
    add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')
    add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')
    end
    function add_hotkey_to_menu(s)
    if Text_cache[s] == nil then
    Text_cache[s] = App.newText(love.graphics.getFont(), s)
    end
    local width = App.width(Text_cache[s])
    if Menu_cursor + width > App.screen.width - 5 then
    return
    end
    App.color(Menu_command_color)
    App.screen.draw(Text_cache[s], Menu_cursor,5)
    Menu_cursor = Menu_cursor + width + 30
    end
    function source.draw_file_navigator()
    for i,file in ipairs(File_navigation.candidates) do
    if file == 'source' then
    App.color(Menu_border_color)
    love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
    end
    add_file_to_menu(file, i == File_navigation.index)
    end
    end
    function add_file_to_menu(s, cursor_highlight)
    if Text_cache[s] == nil then
    Text_cache[s] = App.newText(love.graphics.getFont(), s)
    end
    local width = App.width(Text_cache[s])
    if Menu_cursor + width > App.screen.width - 5 then
    return
    end
    if cursor_highlight then
    App.color(Menu_highlight_color)
    love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)
    end
    App.color(Menu_command_color)
    App.screen.draw(Text_cache[s], Menu_cursor,5)
    Menu_cursor = Menu_cursor + width + 30
    end
    function keychord_pressed_on_file_navigator(chord, key)
    if chord == 'escape' then
    Show_file_navigator = false
    elseif chord == 'return' then
    local candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')
    source.switch_to_file(candidate)
    Show_file_navigator = false
    elseif chord == 'left' then
    if File_navigation.index > 1 then
    File_navigation.index = File_navigation.index-1
    end
    elseif chord == 'right' then
    if File_navigation.index < #File_navigation.candidates then
    File_navigation.index = File_navigation.index+1
    end
    end
    end
  • edit in text.lua at line 420
    [6.657][5.415:476](),[6.657][5.415:476]()
    State.cursor1 = {line=State.cursor1.line-1, pos=nil}
  • resolve order conflict in text.lua at line 420
    [6.4502]
    [6.3974]
  • edit in text.lua at line 450
    [6.6283][5.477:651](),[6.6283][5.477:651]()
    State.cursor1 = {
    line = new_cursor_line,
    pos = Text.nearest_cursor_pos(State.lines[new_cursor_line].data, State.cursor_x, State.left),
    }
  • resolve order conflict in text.lua at line 450
    [6.5714]
    [6.1313]
  • edit in text.lua at line 556
    [6.428][5.723:858](),[6.428][5.723:858]()
    State.cursor1 = {
    line = new_cursor_line,
    pos = utf8.len(State.lines[new_cursor_line].data) + 1,
    }
  • resolve order conflict in text.lua at line 556
    [6.5437]
    [6.6550]
  • edit in text.lua at line 578
    [6.1156][5.859:913](),[6.1156][5.859:913]()
    State.cursor1 = {line=new_cursor_line, pos=1}
  • resolve order conflict in text.lua at line 578
    [6.6919]
    [6.1327]
  • resurrect zombie in text.lua at line 787
    [6.109][5.1426:1460](),[6.109][5.1426:1460]()
    local result = {line=loc1.line}
  • edit in source_undo.lua at line 53
    [6.1850]
    [6.1850]
    current_drawing_mode=Drawing_mode,
    previous_drawing_mode=State.previous_drawing_mode,
  • replacement in source_undo.lua at line 63
    [6.2077][6.2077:2143]()
    table.insert(event.lines, {data=line.data, dataB=line.dataB})
    [6.2077]
    [6.2143]
    if line.mode == 'text' then
    table.insert(event.lines, {mode='text', data=line.data, dataB=line.dataB})
    elseif line.mode == 'drawing' then
    local points=deepcopy(line.points)
    --? print('copying', line.points, 'with', #line.points, 'points into', points)
    local shapes=deepcopy(line.shapes)
    --? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)
    table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}})
    --? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
    else
    print(line.mode)
    assert(false)
    end
  • edit in source_text_tests.lua at line 15
    [6.4242]
    [6.4242]
    end
    function test_click_to_create_drawing()
    io.write('\ntest_click_to_create_drawing')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)
    -- cursor skips drawing to always remain on text
    check_eq(#Editor_state.lines, 2, 'F - test_click_to_create_drawing/#lines')
    check_eq(Editor_state.cursor1.line, 2, 'F - test_click_to_create_drawing/cursor')
    end
    function test_backspace_to_delete_drawing()
    io.write('\ntest_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')
    check_eq(#Editor_state.lines, 1, 'F - test_backspace_to_delete_drawing/#lines')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_to_delete_drawing/cursor')
  • edit in source_text_tests.lua at line 724
    [6.34251]
    [6.34251]
    end
    function test_pagedown_skips_drawings()
    io.write('\ntest_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', 'F - test_pagedown_skips_drawings/baseline/lines')
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    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', 'F - test_pagedown_skips_drawings/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')
    check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown_skips_drawings/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_skips_drawings/cursor')
    y = Editor_state.top + drawing_height
    App.screen.check(y, 'def', 'F - test_pagedown_skips_drawings/screen:1')
  • replacement in source_text_tests.lua at line 1588
    [6.80491][6.80491:80553]()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}
    [6.80491]
    [6.80553]
    Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'}
  • edit in source_text.lua at line 56
    [6.85916][6.85916:86070]()
    --? if Foo then
    --? print('draw:', State.lines[line_index].data, "=====", State.lines[line_index].dataB, 'starting from x', x+AB_padding)
    --? end
  • edit in source_text.lua at line 196
    [6.91548]
    [6.91548]
    if line.mode ~= 'text' then return end
  • edit in source_text.lua at line 223
    [6.92382]
    [6.92382]
    if line.mode ~= 'text' then return end
  • replacement in source_text.lua at line 418
    [6.101828][6.101828:102158]()
    -- 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)
    [6.101828]
    [6.102158]
    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
  • replacement in source_text.lua at line 478
    [6.104958][6.104958:105223]()
    -- join lines
    State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
    -- delete side B on first line
    State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
    [6.104958]
    [6.105223]
    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
    -- delete side B on first line
    State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
    end
  • replacement in source_text.lua at line 539
    [6.106965][6.106965:107132]()
    table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
    [6.106965]
    [6.107132]
    table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
  • replacement in source_text.lua at line 559
    [6.107952][6.107952:107982]()
    y = y - State.line_height
    [6.107952]
    [6.107982]
    if State.lines[State.screen_top1.line].mode == 'text' then
    y = y - State.line_height
    elseif State.lines[State.screen_top1.line].mode == 'drawing' then
    y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
    end
  • replacement in source_text.lua at line 580
    [6.108635][6.108635:108722]()
    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    [6.108635]
    [6.108722]
    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB}
  • edit in source_text.lua at line 591
    [6.109190]
    [6.109190]
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in source_text.lua at line 605
    [6.109662][6.109662:110542]()
    if State.cursor1.line > 1 then
    --? print('found previous text line')
    State.cursor1 = {line=State.cursor1.line-1, 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(s, State.cursor_x, State.left) - 1
    [6.109662]
    [6.110542]
    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=State.cursor1.line-1, 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(s, State.cursor_x, State.left) - 1
    break
    end
  • replacement in source_text.lua at line 645
    [6.111691][6.111691:112426]()
    if State.cursor1.line > 1 then
    State.cursor1.line = State.cursor1.line-1
    State.cursor1.posB = nil
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local prev_line_cache = State.line_cache[State.cursor1.line]
    local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
    local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
    State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    [6.111691]
    [6.112426]
    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=State.cursor1.line-1, posB=nil}
    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
    local prev_line_cache = State.line_cache[State.cursor1.line]
    local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
    local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
    local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
    State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
    break
    end
  • edit in source_text.lua at line 696
    [6.114899]
    [6.114899]
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in source_text.lua at line 701
    [6.115209][6.115209:115465]()
    if State.cursor1.line < #State.lines then
    State.cursor1 = {
    line = State.cursor1.line+1,
    pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line+1].data, State.cursor_x, State.left)
    }
    --? print(State.cursor1.pos)
    [6.115209]
    [6.115465]
    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.lines[new_cursor_line].data, State.cursor_x, State.left),
    }
    --? print(State.cursor1.pos)
    break
    end
  • replacement in source_text.lua at line 766
    [6.118588][6.118588:118654]()
    State.screen_top1 = {line=State.cursor1.line, pos=1} -- copy
    [6.118588]
    [6.118654]
    State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copy
  • replacement in source_text.lua at line 948
    [6.123003][6.123003:123169]()
    elseif State.cursor1.line > 1 then
    State.cursor1 = {
    line = State.cursor1.line-1,
    pos = utf8.len(State.lines[State.cursor1.line-1].data) + 1,
    }
    [6.123003]
    [6.123169]
    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
  • edit in source_text.lua at line 991
    [6.124061]
    [6.124061]
    assert(State.lines[State.cursor1.line].mode == 'text')
  • replacement in source_text.lua at line 1002
    [6.124348][6.124348:124454]()
    elseif State.cursor1.line <= #State.lines-1 then
    State.cursor1 = {line=State.cursor1.line+1, pos=1}
    [6.124348]
    [6.124454]
    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
  • replacement in source_text.lua at line 1017
    [6.124634][6.124634:124685]()
    elseif State.cursor1.line <= #State.lines-1 then
    [6.124634]
    [6.124685]
    else
  • replacement in source_text.lua at line 1019
    [6.124718][6.124718:124773]()
    State.cursor1 = {line=State.cursor1.line+1, pos=1}
    [6.124718]
    [6.124773]
    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
  • replacement in source_text.lua at line 1078
    [6.126781][6.126781:126841]()
    if State.top > App.screen.height - State.line_height then
    [6.126781]
    [6.126841]
    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
    -- 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)
    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
  • replacement in source_text.lua at line 1119
    [6.128059][6.128059:128133]()
    local h = State.line_height
    if y - h < State.top then
    break
    [6.128059]
    [6.128133]
    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)
    assert(State.lines[top2.line-1].mode == 'drawing')
    -- 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
  • edit in source_text.lua at line 1137
    [6.128141][6.128141:128155]()
    y = y - h
  • edit in source_text.lua at line 1144
    [6.128732][6.128732:128745]()
    Foo = true
  • edit in source_text.lua at line 1417
    [6.139954]
    [6.139954]
    if State.lines[loc1.line].mode == 'drawing' then
    return {line=loc1.line, screen_line=1, screen_pos=1}
    end
  • edit in source_text.lua at line 1530
    [6.142698][6.142698:142717]()
    --? print('a')
  • edit in source_text.lua at line 1532
    [6.142820][6.142820:142839]()
    --? print('b')
  • replacement in source_file.lua at line 28
    [6.151025][6.151025:151186]()
    local line_info = {}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    [6.151025]
    [6.151186]
    if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated
    table.insert(result, load_drawing(infile_next_line))
  • replacement in source_file.lua at line 31
    [6.151197][6.151197:151227]()
    line_info.data = line
    [6.151197]
    [6.151227]
    local line_info = {mode='text'}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    else
    line_info.data = line
    end
    table.insert(result, line_info)
  • edit in source_file.lua at line 39
    [6.151237][6.151237:151275]()
    table.insert(result, line_info)
  • replacement in source_file.lua at line 42
    [6.151312][6.151312:151348]()
    table.insert(result, {data=''})
    [6.151312]
    [6.151348]
    table.insert(result, {mode='text', data=''})
  • replacement in source_file.lua at line 53
    [6.151584][6.151584:151714]()
    outfile:write(line.data)
    if line.dataB and #line.dataB > 0 then
    outfile:write(Fold)
    outfile:write(line.dataB)
    [6.151584]
    [6.151714]
    if line.mode == 'drawing' then
    store_drawing(outfile, line)
    else
    outfile:write(line.data)
    if line.dataB and #line.dataB > 0 then
    outfile:write(Fold)
    outfile:write(line.dataB)
    end
    outfile:write('\n')
  • edit in source_file.lua at line 63
    [6.151722][6.151722:151746]()
    outfile:write('\n')
  • edit in source_file.lua at line 65
    [6.151770]
    [6.151936]
    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)
    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
    print(shape.mode)
    assert(false)
    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), '\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, '\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, '\n')
    elseif shape.mode == 'circle' then
    outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\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}), '\n')
    elseif shape.mode == 'deleted' then
    -- ignore
    else
    print(shape.mode)
    assert(false)
    end
    end
    outfile:write('```\n')
  • replacement in source_file.lua at line 141
    [6.152134][6.152134:152289]()
    local line_info = {}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    [6.152134]
    [6.152289]
    --? 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)
  • replacement in source_file.lua at line 148
    [6.152298][6.152298:152326]()
    line_info.data = line
    [6.152298]
    [6.152326]
    --? print('inserting text')
    local line_info = {mode='text'}
    if line:find(Fold) then
    _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')
    else
    line_info.data = line
    end
    table.insert(result, line_info)
  • edit in source_file.lua at line 157
    [6.152334][6.152334:152370]()
    table.insert(result, line_info)
  • replacement in source_file.lua at line 159
    [6.152399][6.152399:152435]()
    table.insert(result, {data=''})
    [6.152399]
    [6.152435]
    table.insert(result, {mode='text', data=''})
  • edit in source_file.lua at line 163
    [6.152461]
    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)
    --? 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
    print(shape.mode)
    assert(false)
    end
    table.insert(drawing.shapes, shape)
    end
    return i, drawing
    end
  • edit in source_edit.lua at line 4
    [6.152609]
    [6.152609]
    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
  • edit in source_edit.lua at line 9
    [6.152736]
    [6.152736]
    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}
  • edit in source_edit.lua at line 18
    [6.152861]
    [6.152861]
    Drawing_padding_top = 10
    Drawing_padding_bottom = 10
    Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
  • edit in source_edit.lua at line 23
    [6.152862]
    [6.152862]
    Same_point_distance = 4 -- pixel distance at which two points are considered the same
  • replacement in source_edit.lua at line 30
    [6.153050][6.153050:153260]()
    -- a line of bifold text consists of an A side and an optional B side, each of which is a string
    -- expanded: whether to show B side
    lines = {{data='', dataB=nil, expanded=nil}}, -- array of lines
    [6.153050]
    [6.153260]
    -- a line is either bifold text or a drawing
    -- a line of bifold text consists of an A side and an optional B side
    -- mode = 'text',
    -- string data,
    -- string dataB,
    -- expanded: whether to show B side
    -- a drawing is a table with:
    -- mode = 'drawing'
    -- a (y) coord in pixels (updated while painting screen),
    -- 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='', dataB=nil, expanded=nil}}, -- array of lines
  • edit in source_edit.lua at line 82
    [6.154867]
    [6.154867]
    current_drawing_mode = 'line',
    previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points
  • edit in source_edit.lua at line 109
    [6.155413]
    [6.155413]
    function edit.fixup_cursor(State)
    for i,line in ipairs(State.lines) do
    if line.mode == 'text' then
    State.cursor1.line = i
    break
    end
    end
    end
  • replacement in source_edit.lua at line 132
    [6.155947][6.155947:155991]()
    --? print('draw:', y, line_index, line)
    [6.155947]
    [6.155991]
    --? print('draw:', y, line_index, line, line.mode)
  • edit in source_edit.lua at line 135
    [6.156119]
    [6.156119]
    if line.mode == 'text' then
  • replacement in source_edit.lua at line 137
    [6.156161][6.156161:156395]()
    local startpos, startposB = 1, nil
    if line_index == State.screen_top1.line then
    if State.screen_top1.pos then
    startpos = State.screen_top1.pos
    else
    startpos, startposB = nil, State.screen_top1.posB
    [6.156161]
    [6.156395]
    local startpos, startposB = 1, nil
    if line_index == State.screen_top1.line then
    if State.screen_top1.pos then
    startpos = State.screen_top1.pos
    else
    startpos, startposB = nil, State.screen_top1.posB
    end
    end
    if line.data == '' then
    -- button to insert new drawing
    button(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,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,
    })
  • edit in source_edit.lua at line 161
    [6.156405]
    [6.156405]
    y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)
    y = y + State.line_height
    --? 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
    print(line.mode)
    assert(false)
  • edit in source_edit.lua at line 172
    [6.156413][6.156413:156582]()
    y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)
    y = y + State.line_height
    --? print('=> y', y)
  • edit in source_edit.lua at line 179
    [6.156691]
    [6.156691]
    Drawing.update(State, dt)
  • replacement in source_edit.lua at line 201
    [6.157214][6.157214:157280]()
    --? print('press', State.selection1.line, State.selection1.pos)
    [6.157214]
    [6.157280]
    --? print('press')
  • replacement in source_edit.lua at line 208
    [6.157490][6.157490:157742]()
    if Text.in_line(State, line_index, x,y) then
    local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
    --? print(x,y, 'setting cursor:', line_index, pos, posB)
    State.cursor1 = {line=line_index, pos=pos, posB=posB}
    break
    [6.157490]
    [6.157742]
    if line.mode == 'text' then
    if Text.in_line(State, line_index, x,y) then
    local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
    --? print(x,y, 'setting cursor:', line_index, pos, posB)
    State.cursor1 = {line=line_index, pos=pos, posB=posB}
    break
    end
    elseif line.mode == 'drawing' then
    local line_cache = State.line_cache[line_index]
    if Drawing.in_drawing(line, line_cache, 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_pressed(State, line_index, x,y, mouse_button)
    break
    end
  • edit in source_edit.lua at line 229
    [6.157816]
    [6.157816]
    if State.search_term then return end
    --? print('release')
    if State.lines.current_drawing then
    Drawing.mouse_released(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
    end
  • edit in source_edit.lua at line 247
    [6.158089]
    [6.158089]
    elseif 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)})
  • replacement in source_edit.lua at line 305
    [6.159838][6.159838:159867]()
    elseif chord == 'C-d' then
    [6.159838]
    [6.159867]
    elseif chord == 'C-i' then
  • edit in source_edit.lua at line 340
    [6.161237]
    [6.161237]
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
  • edit in source_edit.lua at line 354
    [6.161740]
    [6.161740]
    -- invalidate various cached bits of lines
    State.lines.current_drawing = nil
  • replacement in source_edit.lua at line 393
    [6.163162][6.163162:163184]()
    -- dispatch to text
    [6.163162]
    [6.163184]
    -- dispatch to drawing or text
    elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
    -- DON'T reset line_cache.starty here
    local drawing_index, drawing = Drawing.current_drawing(State)
    if drawing_index then
    local before = snapshot(State, drawing_index)
    Drawing.keychord_pressed(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.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)
    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
    schedule_save(State)
  • edit in source.lua at line 18
    [6.166166]
    [6.166166]
    'drawing',
    'help',
  • edit in source.lua at line 25
    [6.166246]
    [6.166246]
    'geom',
    'drawing_tests',
  • replacement in source.lua at line 280
    [6.174901][6.174901:174961]()
    if Editor_state.left <= x and x < Editor_state.right then
    [6.174901]
    [6.174961]
    if x < Editor_state.right + Margin_right then
  • edit in main.lua at line 26
    [6.184680][6.27:27]()
  • resurrect zombie in main.lua at line 26
    [6.184859][5.24812:24813](),[6.184859][5.24812:24813]()
  • resolve order conflict in main.lua at line 26
    [6.184680]
    [5.24812]
  • edit in main.lua at line 27
    [5.24813][5.24813:25117](),[5.24813][5.24813:25117](),[6.27][6.162:163](),[6.144][6.162:163](),[5.25117][6.162:163](),[6.184680][6.162:163](),[6.184859][6.162:163](),[6.455][6.162:163]()
    -- 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')
  • edit in main.lua at line 60
    [6.186660]
    [6.186660]
    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')
  • file addition: icons.lua (----------)
    [6.2]
    icon = {}
    function icon.insert_drawing(button_params)
    local x,y = button_params.x, button_params.y
    App.color(Icon_color)
    love.graphics.rectangle('line', x,y, 12,12)
    love.graphics.line(4,y+6, 16,y+6)
    love.graphics.line(10,y, 10,y+12)
    end
    function icon.freehand(x, y)
    love.graphics.line(x+4,y+7,x+5,y+5)
    love.graphics.line(x+5,y+5,x+7,y+4)
    love.graphics.line(x+7,y+4,x+9,y+3)
    love.graphics.line(x+9,y+3,x+10,y+5)
    love.graphics.line(x+10,y+5,x+12,y+6)
    love.graphics.line(x+12,y+6,x+13,y+8)
    love.graphics.line(x+13,y+8,x+13,y+10)
    love.graphics.line(x+13,y+10,x+14,y+12)
    love.graphics.line(x+14,y+12,x+15,y+14)
    love.graphics.line(x+15,y+14,x+15,y+16)
    end
    function icon.line(x, y)
    love.graphics.line(x+4,y+2, x+16,y+18)
    end
    function icon.manhattan(x, y)
    love.graphics.line(x+4,y+20, x+4,y+2)
    love.graphics.line(x+4,y+2, x+10,y+2)
    love.graphics.line(x+10,y+2, x+10,y+10)
    love.graphics.line(x+10,y+10, x+18,y+10)
    end
    function icon.polygon(x, y)
    love.graphics.line(x+8,y+2, x+14,y+2)
    love.graphics.line(x+14,y+2, x+18,y+10)
    love.graphics.line(x+18,y+10, x+10,y+18)
    love.graphics.line(x+10,y+18, x+4,y+12)
    love.graphics.line(x+4,y+12, x+8,y+2)
    end
    function icon.rectangle(x, y)
    love.graphics.line(x+4,y+8, x+4,y+16)
    love.graphics.line(x+4,y+16, x+16,y+16)
    love.graphics.line(x+16,y+16, x+16,y+8)
    love.graphics.line(x+16,y+8, x+4,y+8)
    end
    function icon.square(x, y)
    love.graphics.line(x+6,y+6, x+6,y+16)
    love.graphics.line(x+6,y+16, x+16,y+16)
    love.graphics.line(x+16,y+16, x+16,y+6)
    love.graphics.line(x+16,y+6, x+6,y+6)
    end
    function icon.circle(x, y)
    love.graphics.circle('line', x+10,y+10, 8)
    end
  • file addition: help.lua (----------)
    [6.2]
    function draw_help_without_mouse_pressed(State, drawing_index)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    App.color(Help_color)
    local y = line_cache.starty+10
    love.graphics.print("Things you can do:", State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", State.left+30,y)
    y = y + State.line_height
    love.graphics.print("then press the mouse button to drop it", State.left+30+bullet_indent(),y)
    y = y + State.line_height
    love.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", State.left+30,y)
    y = y + State.line_height
    if State.current_drawing_mode ~= 'freehand' then
    love.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'line' then
    love.graphics.print("* Press 'ctrl+l' to switch to drawing lines", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'manhattan' then
    love.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'circle' then
    love.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'polygon' then
    love.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'rectangle' then
    love.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'square' then
    love.graphics.print("* Press 'ctrl+s' to switch to drawing squares", State.left+30,y)
    y = y + State.line_height
    end
    love.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", State.left+30,y)
    y = y + State.line_height
    love.graphics.print("Press 'esc' now to hide this message", State.left+30,y)
    y = y + State.line_height
    App.color(Help_background_color)
    love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))
    end
    function draw_help_with_mouse_pressed(State, drawing_index)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    App.color(Help_color)
    local y = line_cache.starty+10
    love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y)
    y = y + State.line_height
    love.graphics.print('Things you can do now:', State.left+30,y)
    y = y + State.line_height
    if State.current_drawing_mode == 'freehand' then
    love.graphics.print('* Release the mouse button to finish drawing the stroke', State.left+30,y)
    y = y + State.line_height
    elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
    love.graphics.print('* Release the mouse button to finish drawing the line', State.left+30,y)
    y = y + State.line_height
    elseif State.current_drawing_mode == 'circle' then
    if drawing.pending.mode == 'circle' then
    love.graphics.print('* Release the mouse button to finish drawing the circle', State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Press 'a' to draw just an arc of a circle", State.left+30,y)
    else
    love.graphics.print('* Release the mouse button to finish drawing the arc', State.left+30,y)
    end
    y = y + State.line_height
    elseif State.current_drawing_mode == 'polygon' then
    love.graphics.print('* Release the mouse button to finish drawing the polygon', State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Press 'p' to add a vertex to the polygon", State.left+30,y)
    y = y + State.line_height
    elseif State.current_drawing_mode == 'rectangle' then
    if #drawing.pending.vertices < 2 then
    love.graphics.print("* Press 'p' to add a vertex to the rectangle", State.left+30,y)
    y = y + State.line_height
    else
    love.graphics.print('* Release the mouse button to finish drawing the rectangle', State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Press 'p' to replace the second vertex of the rectangle", State.left+30,y)
    y = y + State.line_height
    end
    elseif State.current_drawing_mode == 'square' then
    if #drawing.pending.vertices < 2 then
    love.graphics.print("* Press 'p' to add a vertex to the square", State.left+30,y)
    y = y + State.line_height
    else
    love.graphics.print('* Release the mouse button to finish drawing the square', State.left+30,y)
    y = y + State.line_height
    love.graphics.print("* Press 'p' to replace the second vertex of the square", State.left+30,y)
    y = y + State.line_height
    end
    end
    love.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", State.left+30,y)
    y = y + State.line_height
    y = y + State.line_height
    if State.current_drawing_mode ~= 'line' then
    love.graphics.print("* Press 'l' to switch to drawing lines", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'manhattan' then
    love.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'circle' then
    love.graphics.print("* Press 'o' to switch to drawing circles/arcs", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'polygon' then
    love.graphics.print("* Press 'g' to switch to drawing polygons", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'rectangle' then
    love.graphics.print("* Press 'r' to switch to drawing rectangles", State.left+30,y)
    y = y + State.line_height
    end
    if State.current_drawing_mode ~= 'square' then
    love.graphics.print("* Press 's' to switch to drawing squares", State.left+30,y)
    y = y + State.line_height
    end
    App.color(Help_background_color)
    love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))
    end
    function current_shape(State, shape)
    if State.current_drawing_mode == 'freehand' then
    return 'freehand stroke'
    elseif State.current_drawing_mode == 'line' then
    return 'straight line'
    elseif State.current_drawing_mode == 'manhattan' then
    return 'horizontal/vertical line'
    elseif State.current_drawing_mode == 'circle' and shape and shape.start_angle then
    return 'arc'
    else
    return State.current_drawing_mode
    end
    end
    function bullet_indent()
    return App.width(to_text('* '))
    end
  • file addition: geom.lua (----------)
    [6.2]
    geom = {}
    function geom.on_shape(x,y, drawing, shape)
    if shape.mode == 'freehand' then
    return geom.on_freehand(x,y, drawing, shape)
    elseif shape.mode == 'line' then
    return geom.on_line(x,y, drawing, shape)
    elseif shape.mode == 'manhattan' then
    local p1 = drawing.points[shape.p1]
    local p2 = drawing.points[shape.p2]
    if p1.x == p2.x then
    if x ~= p1.x then return false end
    local y1,y2 = p1.y, p2.y
    if y1 > y2 then
    y1,y2 = y2,y1
    end
    return y >= y1-2 and y <= y2+2
    elseif p1.y == p2.y then
    if y ~= p1.y then return false end
    local x1,x2 = p1.x, p2.x
    if x1 > x2 then
    x1,x2 = x2,x1
    end
    return x >= x1-2 and x <= x2+2
    end
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
    return geom.on_polygon(x,y, drawing, shape)
    elseif shape.mode == 'circle' then
    local center = drawing.points[shape.center]
    local dist = geom.dist(center.x,center.y, x,y)
    return dist > shape.radius*0.95 and dist < shape.radius*1.05
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    local dist = geom.dist(center.x,center.y, x,y)
    if dist < shape.radius*0.95 or dist > shape.radius*1.05 then
    return false
    end
    return geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle)
    elseif shape.mode == 'deleted' then
    else
    print(shape.mode)
    assert(false)
    end
    end
    function geom.on_freehand(x,y, drawing, shape)
    local prev
    for _,p in ipairs(shape.points) do
    if prev then
    if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then
    return true
    end
    end
    prev = p
    end
    return false
    end
    function geom.on_line(x,y, drawing, shape)
    local p1,p2
    if type(shape.p1) == 'number' then
    p1 = drawing.points[shape.p1]
    p2 = drawing.points[shape.p2]
    else
    p1 = shape.p1
    p2 = shape.p2
    end
    if p1.x == p2.x then
    if math.abs(p1.x-x) > 2 then
    return false
    end
    local y1,y2 = p1.y,p2.y
    if y1 > y2 then
    y1,y2 = y2,y1
    end
    return y >= y1-2 and y <= y2+2
    end
    -- has the right slope and intercept
    local m = (p2.y - p1.y) / (p2.x - p1.x)
    local yp = p1.y + m*(x-p1.x)
    if yp < y-2 or yp > y+2 then
    return false
    end
    -- between endpoints
    local k = (x-p1.x) / (p2.x-p1.x)
    return k > -0.005 and k < 1.005
    end
    function geom.on_polygon(x,y, drawing, shape)
    local prev
    for _,p in ipairs(shape.vertices) do
    if prev then
    if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then
    return true
    end
    end
    prev = p
    end
    return geom.on_line(x,y, drawing, {p1=shape.vertices[1], p2=shape.vertices[#shape.vertices]})
    end
    -- are (x3,y3) and (x4,y4) on the same side of the line between (x1,y1) and (x2,y2)
    function geom.same_side(x1,y1, x2,y2, x3,y3, x4,y4)
    if x1 == x2 then
    return math.sign(x3-x1) == math.sign(x4-x1)
    end
    if y1 == y2 then
    return math.sign(y3-y1) == math.sign(y4-y1)
    end
    local m = (y2-y1)/(x2-x1)
    return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4)
    end
    function math.sign(x)
    if x > 0 then
    return 1
    elseif x == 0 then
    return 0
    elseif x < 0 then
    return -1
    end
    end
    function geom.angle_with_hint(x1, y1, x2, y2, hint)
    local result = geom.angle(x1,y1, x2,y2)
    if hint then
    -- Smooth the discontinuity where angle goes from positive to negative.
    -- The hint is a memory of which way we drew it last time.
    while result > hint+math.pi/10 do
    result = result-math.pi*2
    end
    while result < hint-math.pi/10 do
    result = result+math.pi*2
    end
    end
    return result
    end
    -- result is from -π/2 to 3π/2, approximately adding math.atan2 from Lua 5.3
    -- (LÖVE is Lua 5.1)
    function geom.angle(x1,y1, x2,y2)
    local result = math.atan((y2-y1)/(x2-x1))
    if x2 < x1 then
    result = result+math.pi
    end
    return result
    end
    -- is the line between x,y and cx,cy at an angle between s and e?
    function geom.angle_between(ox,oy, x,y, s,e)
    local angle = geom.angle(ox,oy, x,y)
    if s > e then
    s,e = e,s
    end
    -- I'm not sure this is right or ideal..
    angle = angle-math.pi*2
    if s <= angle and angle <= e then
    return true
    end
    angle = angle+math.pi*2
    if s <= angle and angle <= e then
    return true
    end
    angle = angle+math.pi*2
    return s <= angle and angle <= e
    end
    function geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 end
  • replacement in file.lua at line 40
    [6.419][6.7955:7990](),[6.16169][5.25126:25183](),[6.16169][5.25126:25183]()
    outfile:write(line.data, '\n')
    outfile:write(line.data)
    outfile:write('\n')
    [6.419]
    [6.16214]
    outfile:write(line.data)
    outfile:write('\n')
  • resurrect zombie in edit.lua at line 93
    [6.3138][6.9505:9658](),[6.3138][6.9505:9658]()
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
  • edit in edit.lua at line 93
    [6.3095]
    [6.9505]
    State.screen_bottom1 = {line=line_index, pos=nil}
  • edit in edit.lua at line 98
    [6.9658][2.13:13](),[6.3095][5.25185:25239](),[6.3095][5.25185:25239]()
    State.screen_bottom1 = {line=line_index, pos=nil}
  • resolve order conflict in edit.lua at line 98
    [6.9658]
    [6.5845]
  • edit in edit.lua at line 103
    [6.5923][6.8115:8245]()
    --? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line].data))
  • file addition: drawing_tests.lua (----------)
    [6.2]
    -- major tests for drawings
    -- We minimize assumptions about specific pixels, and try to test at the level
    -- of specific shapes. In particular, no tests of freehand drawings.
    function test_creating_drawing_saves()
    io.write('\ntest_creating_drawing_saves')
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    -- click on button to create drawing
    edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)
    -- file not immediately saved
    edit.update(Editor_state, 0.01)
    check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- filesystem contains drawing and an empty line of text
    check_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves')
    end
    function test_draw_line()
    io.write('\ntest_draw_line')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_line/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_line/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_line/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_line/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_line/baseline/#shapes')
    -- draw a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_line/#shapes')
    check_eq(#drawing.points, 2, 'F - test_draw_line/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_draw_line/p1:x')
    check_eq(p1.y, 6, 'F - test_draw_line/p1:y')
    check_eq(p2.x, 35, 'F - test_draw_line/p2:x')
    check_eq(p2.y, 36, 'F - test_draw_line/p2:y')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- The format on disk isn't perfectly stable. Table fields can be reordered.
    -- So just reload from disk to verify.
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_line/save/#shapes')
    check_eq(#drawing.points, 2, 'F - test_draw_line/save/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/save/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_draw_line/save/p1:x')
    check_eq(p1.y, 6, 'F - test_draw_line/save/p1:y')
    check_eq(p2.x, 35, 'F - test_draw_line/save/p2:x')
    check_eq(p2.y, 36, 'F - test_draw_line/save/p2:y')
    end
    function test_draw_horizontal_line()
    io.write('\ntest_draw_horizontal_line')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'manhattan'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_horizontal_line/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_horizontal_line/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_horizontal_line/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_horizontal_line/baseline/#shapes')
    -- draw a line that is more horizontal than vertical
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_horizontal_line/#shapes')
    check_eq(#drawing.points, 2, 'F - test_draw_horizontal_line/#points')
    check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_draw_horizontal_line/shape_mode')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_draw_horizontal_line/p1:x')
    check_eq(p1.y, 6, 'F - test_draw_horizontal_line/p1:y')
    check_eq(p2.x, 35, 'F - test_draw_horizontal_line/p2:x')
    check_eq(p2.y, p1.y, 'F - test_draw_horizontal_line/p2:y')
    end
    function test_draw_circle()
    io.write('\ntest_draw_circle')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_circle/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle/baseline/#shapes')
    -- draw a circle
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    edit.run_after_keychord(Editor_state, 'C-o')
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_circle/#shapes')
    check_eq(#drawing.points, 1, 'F - test_draw_circle/#points')
    check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')
    check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle/radius')
    local center = drawing.points[drawing.shapes[1].center]
    check_eq(center.x, 35, 'F - test_draw_circle/center:x')
    check_eq(center.y, 36, 'F - test_draw_circle/center:y')
    end
    function test_cancel_stroke()
    io.write('\ntest_cancel_stroke')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_cancel_stroke/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_cancel_stroke/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_cancel_stroke/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_cancel_stroke/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_cancel_stroke/baseline/#shapes')
    -- start drawing a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    -- cancel
    edit.run_after_keychord(Editor_state, 'escape')
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes')
    end
    function test_keys_do_not_affect_shape_when_mouse_up()
    io.write('\ntest_keys_do_not_affect_shape_when_mouse_up')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- hover over drawing and press 'o' without holding mouse
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    edit.run_after_keychord(Editor_state, 'o')
    -- no change to drawing mode
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode')
    -- no change to text either because we didn't run the textinput event
    end
    function test_draw_circle_mid_stroke()
    io.write('\ntest_draw_circle_mid_stroke')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes')
    -- draw a circle
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_keychord(Editor_state, 'o')
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes')
    check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points')
    check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')
    check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius')
    local center = drawing.points[drawing.shapes[1].center]
    check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x')
    check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y')
    end
    function test_draw_arc()
    io.write('\ntest_draw_arc')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'circle'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes')
    -- draw an arc
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    App.mouse_move(Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'a') -- arc mode
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35+50, Editor_state.top+Drawing_padding_top+36+50, 1) -- 45°
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes')
    check_eq(#drawing.points, 1, 'F - test_draw_arc/#points')
    check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode')
    local arc = drawing.shapes[1]
    check_eq(arc.radius, 30, 'F - test_draw_arc/radius')
    local center = drawing.points[arc.center]
    check_eq(center.x, 35, 'F - test_draw_arc/center:x')
    check_eq(center.y, 36, 'F - test_draw_arc/center:y')
    check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle')
    check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle')
    end
    function test_draw_polygon()
    io.write('\ntest_draw_polygon')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- final point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes')
    check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices')
    local shape = drawing.shapes[1]
    check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode')
    check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices')
    local p = drawing.points[shape.vertices[1]]
    check_eq(p.x, 5, 'F - test_draw_polygon/p1:x')
    check_eq(p.y, 6, 'F - test_draw_polygon/p1:y')
    local p = drawing.points[shape.vertices[2]]
    check_eq(p.x, 65, 'F - test_draw_polygon/p2:x')
    check_eq(p.y, 36, 'F - test_draw_polygon/p2:y')
    local p = drawing.points[shape.vertices[3]]
    check_eq(p.x, 35, 'F - test_draw_polygon/p3:x')
    check_eq(p.y, 26, 'F - test_draw_polygon/p3:y')
    end
    function test_draw_rectangle()
    io.write('\ntest_draw_rectangle')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_keychord(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    edit.run_after_keychord(Editor_state, 'p')
    -- release (decides 'thickness' of rectangle perpendicular to first edge)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes')
    check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point added
    local shape = drawing.shapes[1]
    check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode')
    check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices')
    local p = drawing.points[shape.vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x')
    check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y')
    local p = drawing.points[shape.vertices[2]]
    check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x')
    check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y')
    local p = drawing.points[shape.vertices[3]]
    check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x')
    check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y')
    local p = drawing.points[shape.vertices[4]]
    check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x')
    check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y')
    end
    function test_draw_rectangle_intermediate()
    io.write('\ntest_draw_rectangle_intermediate')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_keychord(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    edit.run_after_keychord(Editor_state, 'p')
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point added
    local pending = drawing.pending
    check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode')
    check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices')
    local p = drawing.points[pending.vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x')
    check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y')
    local p = drawing.points[pending.vertices[2]]
    check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x')
    check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y')
    -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far
    end
    function test_draw_square()
    io.write('\ntest_draw_square')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_keychord(Editor_state, 's') -- square mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_keychord(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66)
    edit.run_after_keychord(Editor_state, 'p')
    -- release (decides which side of first edge to draw square on)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes')
    check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point added
    check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode')
    check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices')
    local p = drawing.points[drawing.shapes[1].vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_square/p1:x')
    check_eq(p.y, 36, 'F - test_draw_square/p1:y')
    local p = drawing.points[drawing.shapes[1].vertices[2]]
    check_eq(p.x, 65, 'F - test_draw_square/p2:x')
    check_eq(p.y, 66, 'F - test_draw_square/p2:y')
    local p = drawing.points[drawing.shapes[1].vertices[3]]
    check_eq(p.x, 35, 'F - test_draw_square/p3:x')
    check_eq(p.y, 96, 'F - test_draw_square/p3:y')
    local p = drawing.points[drawing.shapes[1].vertices[4]]
    check_eq(p.x, 5, 'F - test_draw_square/p4:x')
    check_eq(p.y, 66, 'F - test_draw_square/p4:y')
    end
    function test_name_point()
    io.write('\ntest_name_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- draw a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y')
    check_nil(p2.name, 'F - test_name_point/baseline/p2:name')
    -- enter 'name' mode without moving the mouse
    edit.run_after_keychord(Editor_state, 'C-n')
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1')
    edit.run_after_textinput(Editor_state, 'A')
    check_eq(p2.name, 'A', 'F - test_name_point')
    -- still in 'name' mode
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2')
    -- exit 'name' mode
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3')
    check_eq(p2.name, 'A', 'F - test_name_point')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- change is saved
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    check_eq(p2.name, 'A', 'F - test_name_point/save')
    end
    function test_move_point()
    io.write('\ntest_move_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- line is saved to disk
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local drawing = Editor_state.lines[1]
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    check_eq(p2.x, 35, 'F - test_move_point/save/x')
    check_eq(p2.y, 36, 'F - test_move_point/save/y')
    edit.draw(Editor_state)
    -- enter 'move' mode without moving the mouse
    edit.run_after_keychord(Editor_state, 'C-u')
    check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point/mode:1')
    -- point is lifted
    check_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2')
    check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target')
    -- move point
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    edit.update(Editor_state, 0.05)
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p2.x, 26, 'F - test_move_point/x')
    check_eq(p2.y, 44, 'F - test_move_point/y')
    -- exit 'move' mode
    edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_move_point/mode:3')
    check_eq(drawing.pending, {}, 'F - test_move_point/pending')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- change is saved
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    check_eq(p2.x, 26, 'F - test_move_point/save/x')
    check_eq(p2.y, 44, 'F - test_move_point/save/y')
    end
    function test_move_point_on_manhattan_line()
    io.write('\ntest_move_point_on_manhattan_line')
    -- create a drawing with a manhattan line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'manhattan'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+46, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1')
    edit.draw(Editor_state)
    -- enter 'move' mode
    edit.run_after_keychord(Editor_state, 'C-u')
    check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1')
    -- move point
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    edit.update(Editor_state, 0.05)
    -- line is no longer manhattan
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1')
    end
    function test_delete_lines_at_point()
    io.write('\ntest_delete_lines_at_point')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1')
    check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2')
    -- hover on the common point and delete
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'C-d')
    check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1')
    check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2')
    -- wait for some time
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- deleted points disappear after file is reloaded
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save')
    end
    function test_delete_line_under_mouse_pointer()
    io.write('\ntest_delete_line_under_mouse_pointer')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1')
    check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2')
    -- hover on one of the lines and delete
    App.mouse_move(Editor_state.left+25, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_keychord(Editor_state, 'C-d')
    -- only that line is deleted
    check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1')
    check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2')
    end
    function test_delete_point_from_polygon()
    io.write('\ntest_delete_point_from_polygon')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- third point
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- fourth point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
    check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices')
    -- hover on a point and delete
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_keychord(Editor_state, 'C-d')
    -- just the one point is deleted
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape')
    check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices')
    end
    function test_delete_point_from_polygon()
    io.write('\ntest_delete_point_from_polygon')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_keychord(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'p') -- add point
    -- third point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
    check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices')
    -- hover on a point and delete
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'C-d')
    -- there's < 3 points left, so the whole polygon is deleted
    check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon')
    end
    function test_undo_name_point()
    io.write('\ntest_undo_name_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- draw a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y')
    check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name')
    check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1')
    --? print('a', Editor_state.lines.current_drawing)
    -- enter 'name' mode without moving the mouse
    edit.run_after_keychord(Editor_state, 'C-n')
    edit.run_after_textinput(Editor_state, 'A')
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline')
    check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2')
    check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history')
    --? print('b', Editor_state.lines.current_drawing)
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history')
    check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- undo is saved
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    check_eq(p2.name, '', 'F - test_undo_name_point/save')
    end
    function test_undo_move_point()
    io.write('\ntest_undo_move_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y')
    check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name')
    -- move p2
    edit.run_after_keychord(Editor_state, 'C-u')
    App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)
    edit.update(Editor_state, 0.05)
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p2.x, 26, 'F - test_undo_move_point/x')
    check_eq(p2.y, 44, 'F - test_undo_move_point/y')
    -- exit 'move' mode
    edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)
    check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history')
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    edit.run_after_keychord(Editor_state, 'C-z') -- bug: need to undo twice
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history')
    check_eq(p2.x, 35, 'F - test_undo_move_point/x')
    check_eq(p2.y, 36, 'F - test_undo_move_point/y')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- undo is saved
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]
    check_eq(p2.x, 35, 'F - test_undo_move_point/save/x')
    check_eq(p2.y, 36, 'F - test_undo_move_point/save/y')
    end
    function test_undo_delete_point()
    io.write('\ntest_undo_delete_point')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1')
    check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2')
    -- hover on the common point and delete
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'C-d')
    check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1')
    check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2')
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1')
    check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2')
    -- wait until save
    App.wait_fake_time(3.1)
    edit.update(Editor_state, 0)
    -- undo is saved
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save')
    end
  • file addition: drawing.lua (----------)
    [6.2]
    -- primitives for editing drawings
    Drawing = {}
    require 'drawing_tests'
    -- All drawings span 100% of some conceptual 'page width' and divide it up
    -- into 256 parts.
    function Drawing.draw(State, line_index, y)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    line_cache.starty = y
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) then
    App.color(Icon_color)
    love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width))
    if icon[State.current_drawing_mode] then
    icon[State.current_drawing_mode](State.right-22, line_cache.starty+4)
    else
    icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4)
    end
    if App.mouse_down(1) and love.keyboard.isDown('h') then
    draw_help_with_mouse_pressed(State, line_index)
    return
    end
    end
    if line.show_help then
    draw_help_without_mouse_pressed(State, line_index)
    return
    end
    local mx = Drawing.coord(pmx-State.left, State.width)
    local my = Drawing.coord(pmy-line_cache.starty, State.width)
    for _,shape in ipairs(line.shapes) do
    assert(shape)
    if geom.on_shape(mx,my, line, shape) then
    App.color(Focus_stroke_color)
    else
    App.color(Stroke_color)
    end
    Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right)
    end
    local function px(x) return Drawing.pixels(x, State.width)+State.left end
    local function py(y) return Drawing.pixels(y, State.width)+line_cache.starty end
    for i,p in ipairs(line.points) do
    if p.deleted == nil then
    if Drawing.near(p, mx,my, State.width) then
    App.color(Focus_stroke_color)
    love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance)
    else
    App.color(Stroke_color)
    love.graphics.circle('fill', px(p.x),py(p.y), 2)
    end
    if p.name then
    -- TODO: clip
    local x,y = px(p.x)+5, py(p.y)+5
    love.graphics.print(p.name, x,y)
    if State.current_drawing_mode == 'name' and i == line.pending.target_point then
    -- create a faint red box for the name
    App.color(Current_name_background_color)
    local name_text
    -- TODO: avoid computing name width on every repaint
    if p.name == '' then
    name_text = State.em
    else
    name_text = App.newText(love.graphics.getFont(), p.name)
    end
    love.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height)
    end
    end
    end
    end
    App.color(Current_stroke_color)
    Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right)
    end
    function Drawing.draw_shape(drawing, shape, top, left,right)
    local width = right-left
    local function px(x) return Drawing.pixels(x, width)+left end
    local function py(y) return Drawing.pixels(y, width)+top end
    if shape.mode == 'freehand' then
    local prev = nil
    for _,point in ipairs(shape.points) do
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y))
    end
    prev = point
    end
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
    local p1 = drawing.points[shape.p1]
    local p2 = drawing.points[shape.p2]
    love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y))
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
    local prev = nil
    for _,point in ipairs(shape.vertices) do
    local curr = drawing.points[point]
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    end
    prev = curr
    end
    -- close the loop
    local curr = drawing.points[shape.vertices[1]]
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    elseif shape.mode == 'circle' then
    -- TODO: clip
    local center = drawing.points[shape.center]
    love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width))
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
    elseif shape.mode == 'deleted' then
    -- ignore
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.draw_pending_shape(drawing, top, left,right)
    local width = right-left
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    local function px(x) return Drawing.pixels(x, width)+left end
    local function py(y) return Drawing.pixels(y, width)+top end
    local mx = Drawing.coord(pmx-left, width)
    local my = Drawing.coord(pmy-top, width)
    -- recreate pixels from coords to precisely mimic how the drawing will look
    -- after mouse_released
    pmx,pmy = px(mx), py(my)
    local shape = drawing.pending
    if shape.mode == nil then
    -- nothing pending
    elseif shape.mode == 'freehand' then
    local shape_copy = deepcopy(shape)
    Drawing.smoothen(shape_copy)
    Drawing.draw_shape(drawing, shape_copy, top, left,right)
    elseif shape.mode == 'line' then
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local p1 = drawing.points[shape.p1]
    love.graphics.line(px(p1.x),py(p1.y), pmx,pmy)
    elseif shape.mode == 'manhattan' then
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local p1 = drawing.points[shape.p1]
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y))
    else
    love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy)
    end
    elseif shape.mode == 'polygon' then
    -- don't close the loop on a pending polygon
    local prev = nil
    for _,point in ipairs(shape.vertices) do
    local curr = drawing.points[point]
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    end
    prev = curr
    end
    love.graphics.line(px(prev.x),py(prev.y), pmx,pmy)
    elseif shape.mode == 'rectangle' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    return
    end
    local second = drawing.points[shape.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
    love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))
    love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))
    love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))
    love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))
    elseif shape.mode == 'square' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    return
    end
    local second = drawing.points[shape.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
    love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))
    love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))
    love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))
    love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))
    elseif shape.mode == 'circle' then
    local center = drawing.points[shape.center]
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local cx,cy = px(center.x), py(center.y)
    love.graphics.circle('line', cx,cy, geom.dist(cx,cy, App.mouse_x(),App.mouse_y()))
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    shape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle)
    local cx,cy = px(center.x), py(center.y)
    love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
    elseif shape.mode == 'move' then
    -- nothing pending; changes are immediately committed
    elseif shape.mode == 'name' then
    -- nothing pending; changes are immediately committed
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.in_drawing(drawing, line_cache, x,y, left,right)
    if line_cache.starty == nil then return false end -- outside current page
    local width = right-left
    return y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
    end
    function Drawing.mouse_pressed(State, drawing_index, x,y, mouse_button)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    local cx = Drawing.coord(x-State.left, State.width)
    local cy = Drawing.coord(y-line_cache.starty, State.width)
    if State.current_drawing_mode == 'freehand' then
    drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}}
    elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, p1=j}
    elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, vertices={j}}
    elseif State.current_drawing_mode == 'circle' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, center=j}
    elseif State.current_drawing_mode == 'move' then
    -- all the action is in mouse_released
    elseif State.current_drawing_mode == 'name' then
    -- nothing
    else
    print(State.current_drawing_mode)
    assert(false)
    end
    end
    -- a couple of operations on drawings need to constantly check the state of the mouse
    function Drawing.update(State)
    if State.lines.current_drawing == nil then return end
    local drawing = State.lines.current_drawing
    local line_cache = State.line_cache[State.lines.current_drawing_index]
    assert(drawing.mode == 'drawing')
    local pmx, pmy = App.mouse_x(), App.mouse_y()
    local mx = Drawing.coord(pmx-State.left, State.width)
    local my = Drawing.coord(pmy-line_cache.starty, State.width)
    if App.mouse_down(1) then
    if Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) then
    if drawing.pending.mode == 'freehand' then
    table.insert(drawing.pending.points, {x=mx, y=my})
    elseif drawing.pending.mode == 'move' then
    drawing.pending.target_point.x = mx
    drawing.pending.target_point.y = my
    Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
    end
    end
    elseif State.current_drawing_mode == 'move' then
    if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then
    drawing.pending.target_point.x = mx
    drawing.pending.target_point.y = my
    Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
    end
    else
    -- do nothing
    end
    end
    function Drawing.relax_constraints(drawing, p)
    for _,shape in ipairs(drawing.shapes) do
    if shape.mode == 'manhattan' then
    if shape.p1 == p then
    shape.mode = 'line'
    elseif shape.p2 == p then
    shape.mode = 'line'
    end
    elseif shape.mode == 'rectangle' or shape.mode == 'square' then
    for _,v in ipairs(shape.vertices) do
    if v == p then
    shape.mode = 'polygon'
    end
    end
    end
    end
    end
    function Drawing.mouse_released(State, x,y, mouse_button)
    if State.current_drawing_mode == 'move' then
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    if State.lines.current_drawing then
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    end
    elseif State.lines.current_drawing then
    local drawing = State.lines.current_drawing
    local line_cache = State.line_cache[State.lines.current_drawing_index]
    if drawing.pending then
    if drawing.pending.mode == nil then
    -- nothing pending
    elseif drawing.pending.mode == 'freehand' then
    -- the last point added during update is good enough
    Drawing.smoothen(drawing.pending)
    table.insert(drawing.shapes, drawing.pending)
    elseif drawing.pending.mode == 'line' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'manhattan' then
    local p1 = drawing.points[drawing.pending.p1]
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width)
    else
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width)
    end
    local p2 = drawing.points[drawing.pending.p2]
    App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'polygon' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'rectangle' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local first = drawing.points[drawing.pending.vertices[1]]
    local second = drawing.points[drawing.pending.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    else
    -- too few points; draw nothing
    end
    elseif drawing.pending.mode == 'square' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local first = drawing.points[drawing.pending.vertices[1]]
    local second = drawing.points[drawing.pending.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    end
    elseif drawing.pending.mode == 'circle' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local center = drawing.points[drawing.pending.center]
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'arc' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local center = drawing.points[drawing.pending.center]
    drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle)
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'name' then
    -- drop it
    else
    print(drawing.pending.mode)
    assert(false)
    end
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    end
    end
    end
    function Drawing.keychord_pressed(State, chord)
    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
    State.current_drawing_mode = 'line'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.p1 = drawing.pending.vertices[1]
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.p1 = drawing.pending.center
    end
    drawing.pending.mode = 'line'
    elseif chord == 'C-l' and not App.mouse_down(1) then
    State.current_drawing_mode = 'line'
    elseif App.mouse_down(1) and chord == 'm' then
    State.current_drawing_mode = 'manhattan'
    local drawing = Drawing.select_drawing_at_mouse(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'line' then
    -- do nothing
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.p1 = drawing.pending.vertices[1]
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.p1 = drawing.pending.center
    end
    drawing.pending.mode = 'manhattan'
    elseif chord == 'C-m' and not App.mouse_down(1) then
    State.current_drawing_mode = 'manhattan'
    elseif chord == 'C-g' and not App.mouse_down(1) then
    State.current_drawing_mode = 'polygon'
    elseif App.mouse_down(1) and chord == 'g' then
    State.current_drawing_mode = 'polygon'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    -- reuse existing vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'polygon'
    elseif chord == 'C-r' and not App.mouse_down(1) then
    State.current_drawing_mode = 'rectangle'
    elseif App.mouse_down(1) and chord == 'r' then
    State.current_drawing_mode = 'rectangle'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then
    -- reuse existing (1-2) vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'rectangle'
    elseif chord == 'C-s' and not App.mouse_down(1) then
    State.current_drawing_mode = 'square'
    elseif App.mouse_down(1) and chord == 's' then
    State.current_drawing_mode = 'square'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'polygon' then
    while #drawing.pending.vertices > 2 do
    table.remove(drawing.pending.vertices)
    end
    elseif drawing.pending.mode == 'rectangle' then
    -- reuse existing (1-2) vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'square'
    elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    table.insert(drawing.pending.vertices, j)
    elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    while #drawing.pending.vertices >= 2 do
    table.remove(drawing.pending.vertices)
    end
    table.insert(drawing.pending.vertices, j)
    elseif chord == 'C-o' and not App.mouse_down(1) then
    State.current_drawing_mode = 'circle'
    elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    drawing.pending.mode = 'arc'
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local center = drawing.points[drawing.pending.center]
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my)
    elseif App.mouse_down(1) and chord == 'o' then
    State.current_drawing_mode = 'circle'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    drawing.pending.center = drawing.pending.p1
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.center = drawing.pending.vertices[1]
    end
    drawing.pending.mode = 'circle'
    elseif chord == 'C-u' and not App.mouse_down(1) then
    local drawing_index,drawing,line_cache,i,p = Drawing.select_point_at_mouse(State)
    if drawing then
    if State.previous_drawing_mode == nil then
    State.previous_drawing_mode = State.current_drawing_mode
    end
    State.current_drawing_mode = 'move'
    drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i}
    State.lines.current_drawing_index = drawing_index
    State.lines.current_drawing = drawing
    end
    elseif chord == 'C-n' and not App.mouse_down(1) then
    local drawing_index,drawing,line_cache,point_index,p = Drawing.select_point_at_mouse(State)
    if drawing then
    if State.previous_drawing_mode == nil then
    -- don't clobber
    State.previous_drawing_mode = State.current_drawing_mode
    end
    State.current_drawing_mode = 'name'
    p.name = ''
    drawing.pending = {mode=State.current_drawing_mode, target_point=point_index}
    State.lines.current_drawing_index = drawing_index
    State.lines.current_drawing = drawing
    end
    elseif chord == 'C-d' and not App.mouse_down(1) then
    local _,drawing,_,i,p = Drawing.select_point_at_mouse(State)
    if drawing then
    for _,shape in ipairs(drawing.shapes) do
    if Drawing.contains_point(shape, i) then
    if shape.mode == 'polygon' then
    local idx = table.find(shape.vertices, i)
    assert(idx)
    table.remove(shape.vertices, idx)
    if #shape.vertices < 3 then
    shape.mode = 'deleted'
    end
    else
    shape.mode = 'deleted'
    end
    end
    end
    drawing.points[i].deleted = true
    end
    local drawing,_,_,shape = Drawing.select_shape_at_mouse(State)
    if drawing then
    shape.mode = 'deleted'
    end
    elseif chord == 'C-h' and not App.mouse_down(1) then
    local drawing = Drawing.select_drawing_at_mouse(State)
    if drawing then
    drawing.show_help = true
    end
    elseif chord == 'escape' and App.mouse_down(1) then
    local _,drawing = Drawing.current_drawing(State)
    drawing.pending = {}
    end
    end
    function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)
    if firstx == secondx then
    return x,secondy, x,firsty
    end
    if firsty == secondy then
    return secondx,y, firstx,y
    end
    local first_slope = (secondy-firsty)/(secondx-firstx)
    -- slope of second edge:
    -- -1/first_slope
    -- equation of line containing the second edge:
    -- y-secondy = -1/first_slope*(x-secondx)
    -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
    -- now we want to find the point on this line that's closest to the mouse pointer.
    -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
    local a = 1/first_slope
    local c = -secondy - secondx/first_slope
    local thirdx = round(((x-a*y) - a*c) / (a*a + 1))
    local thirdy = round((a*(-x + a*y) - c) / (a*a + 1))
    -- slope of third edge = first_slope
    -- equation of line containing third edge:
    -- y - thirdy = first_slope*(x-thirdx)
    -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
    -- now we want to find the point on this line that's closest to the first point
    local a = -first_slope
    local c = -thirdy + thirdx*first_slope
    local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1))
    local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1))
    return thirdx,thirdy, fourthx,fourthy
    end
    function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y)
    -- use x,y only to decide which side of the first edge to complete the square on
    local deltax = secondx-firstx
    local deltay = secondy-firsty
    local thirdx = secondx+deltay
    local thirdy = secondy-deltax
    if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then
    deltax = -deltax
    deltay = -deltay
    thirdx = secondx+deltay
    thirdy = secondy-deltax
    end
    local fourthx = firstx+deltay
    local fourthy = firsty-deltax
    return thirdx,thirdy, fourthx,fourthy
    end
    function Drawing.current_drawing(State)
    local x, y = App.mouse_x(), App.mouse_y()
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    return drawing_index,drawing,line_cache
    end
    end
    end
    return nil
    end
    function Drawing.select_shape_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    for i,shape in ipairs(drawing.shapes) do
    assert(shape)
    if geom.on_shape(mx,my, drawing, shape) then
    return drawing,line_cache,i,shape
    end
    end
    end
    end
    end
    end
    function Drawing.select_point_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    for i,point in ipairs(drawing.points) do
    assert(point)
    if Drawing.near(point, mx,my, State.width) then
    return drawing_index,drawing,line_cache,i,point
    end
    end
    end
    end
    end
    end
    function Drawing.select_drawing_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    return drawing
    end
    end
    end
    end
    function Drawing.contains_point(shape, p)
    if shape.mode == 'freehand' then
    -- not supported
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
    return shape.p1 == p or shape.p2 == p
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
    return table.find(shape.vertices, p)
    elseif shape.mode == 'circle' then
    return shape.center == p
    elseif shape.mode == 'arc' then
    return shape.center == p
    -- ugh, how to support angles
    elseif shape.mode == 'deleted' then
    -- already done
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.smoothen(shape)
    assert(shape.mode == 'freehand')
    for _=1,7 do
    for i=2,#shape.points-1 do
    local a = shape.points[i-1]
    local b = shape.points[i]
    local c = shape.points[i+1]
    b.x = round((a.x + b.x + c.x)/3)
    b.y = round((a.y + b.y + c.y)/3)
    end
    end
    end
    function round(num)
    return math.floor(num+.5)
    end
    function Drawing.find_or_insert_point(points, x,y, width)
    -- check if UI would snap the two points together
    for i,point in ipairs(points) do
    if Drawing.near(point, x,y, width) then
    return i
    end
    end
    table.insert(points, {x=x, y=y})
    return #points
    end
    function Drawing.near(point, x,y, width)
    local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width)
    local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width)
    return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance
    end
    function Drawing.pixels(n, width) -- parts to pixels
    return math.floor(n*width/256)
    end
    function Drawing.coord(n, width) -- pixels to parts
    return math.floor(n*256/width)
    end
    function table.find(h, x)
    for k,v in pairs(h) do
    if v == x then
    return k
    end
    end
    end
  • replacement in commands.lua at line 31
    [6.204880][6.204880:204938]()
    add_hotkey_to_menu('ctrl+d: create/edit debug print')
    [6.204880]
    [6.204938]
    add_hotkey_to_menu('ctrl+i: create/edit debug print')