editing source code from within the app

[?]
Sep 3, 2022, 8:54 PM
JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC

Dependencies

  • [2] UEE5W7WJ document one more shortcut
  • [3] 7Q6GKOOL .
  • [4] TXDMRA5J bugfix: alt-tab shouldn't emit keypress events
  • [5] AGJXIDOF hardcode some assumptions about how this app uses love
  • [6] 5F54FYKI Merge lines.love
  • [7] Z6HI3K55 correct a comment
  • [8] VNTRXQSX Merge lines.love
  • [9] 2Y7YH7UP infrastructure for caching LÖVE text objects
  • [10] KCIM5UTV revert: back to freehand
  • [11] PGZJ6NAT ensure Filename is writable when opened outside a terminal
  • [12] ESETRNLB bugfix: printing the first part of a line at the bottom made it seem non-wrapping
  • [13] AD34IX2Z couple more tests
  • [14] KJKKASHZ reduce ambitions a bit: page up/down need not start screen from the middle of a line
  • [15] TVCPXAAU rename
  • [16] ETM7ENJR some helpers
  • [17] VVXVV2D2 change data model; text can now have metadata
  • [18] LXTTOB33 extract a couple of files
  • [19] AJB4LFRB try to maintain a reasonable line width
  • [20] G6OYAYHU paste in text with M-v
  • [21] BULPIBEG beginnings of a module for the text editor
  • [22] 46ASCE5K first commandline arg: window dimensions
  • [23] MD3W5IRA new fork: rip out drawing support
  • [24] DWZK32YD include CWD when saving filename
  • [25] GUOQRUL7 Merge lines.love
  • [26] YMH3YXXI title
  • [27] 3TTAYXPP cleanup
  • [28] VXORMHME delete experimental REPL
  • [29] FS2ITYYH record a known issue
  • [30] HRWN5V6J Devine's suggestion to try to live with just freehand
  • [31] OYXDYPGS get rid of debug variables
  • [32] 2ENZW7TV select text using mouse drag
  • [33] K2X6G75Z start writing some tests for drawings
  • [34] UYRAO73Y enable pressing and holding backspace
  • [35] 4CTZOJPC stop pretending globals are local
  • [36] 3HDWCPDI bugfix: include shift keys in modifier_down
  • [37] V7LATJC7 bugfix: resize
  • [38] YT5P6TO6 bugfix: save previous file when dropping a new one on
  • [39] 2L5MEZV3 experiment: new edit namespace
  • [40] HYEAFRZ2 split mouse_pressed events between Text and Drawing
  • [41] AVLAYODP much simpler
  • [42] LUNH47XX make text and drawings the same width
  • [43] ELIVOJ4N bugfix: zoom in/out hotkeys
  • [44] OTIBCAUJ love2d scaffold
  • [45] 5T2E3PDV couple of bugfixes to file-handling
  • [46] 6PUNJS5B backspace
  • [47] WQOSZSUE warn on unused commandline args
  • [48] HPVT467W initialize contains test state
  • [49] OGUV4HSA remove some memory leaks from rendered fragments
  • [50] T4FRZSYL delete an ancient, unused file
  • [51] 4VKEE43Z bugfix
  • [52] Z4XRNDTR find text
  • [53] MGOQ5XAV start uppercasing globals
  • [54] 73OCE2MC after much struggle, a brute-force undo
  • [55] XNFTJHC4 split keyboard handling between Text and Drawing
  • [56] 66X36NZN a little more prose describing manual_tests
  • [57] AVTNUQYR basic test-enabled framework
  • [58] 7EQLPB3O bugfix: don't delete selection when moving cursor
  • [59] MNWHXPBL more lightweight; select just the stroke at the mouse
  • [60] PESSMQBJ no, make sure to compute line width after screen dimensions
  • [61] MHOUX5JF experiment: turn off JIT
  • [62] 5MR22SGZ bugfix: enable resize when loading settings
  • [63] JVRL5TWL store device-independent coordinates inside drawings
  • [64] UH4YWHW5 button framework is at the app level
  • [65] 5FW7YOFT highlight selection while dragging
  • [66] 7PZ4CQFV search: transparently handle drawings everywhere
  • [67] H2DPLWMV snapshot: wrapping long lines at word boundaries
  • [68] 4AXV2HG4 all pending manual tests done!
  • [69] 2KRK3OBV don't rely on defaults
  • [70] NX3DDSCZ fix a variable name
  • [71] U76D4P36 fix a typo
  • [72] 6VXO3ZL3 just keep the cursor visible after any input events
  • [73] LNUHQOGH start passing in Editor_state explicitly
  • [74] ERQKFTPV extract method
  • [75] ZLJGZYQG select text with shift + mouseclick
  • [76] GQSGEYSM .
  • [77] VO3GEIRW use app name in window title
  • [78] K464QQR4 more defensive resize handling
  • [79] KVHUFUFV reorg
  • [80] AM42E4Y6 avoid redundant writes on exit
  • [81] M6JI7UJ7 obsolete comment
  • [82] ZUOL7X6V move
  • [83] PFT5Y2ZY move
  • [84] PWHZPJJM always show current filename in window title
  • [85] QGO66DNK initialize screen width to something reasonable
  • [86] 2C7CTIQY make space for multiple kinds of width
  • [87] UHB4GARJ left/right margin -> left/right coordinates
  • [88] ILOA5BYF separate data structure for each line's cache data
  • [89] S7ZZA3YE ugh, handle absolute as well as relative paths
  • [90] Z4KNS42N to open a file without a terminal, drag it on!
  • [91] 6LJZN727 handle chords
  • [92] XSLCFVFH .
  • [93] IYW7X3WL left/right cursor movement, deleting characters
  • [94] 3RGHOJ25 DRY some code
  • [95] BLWAYPKV extract a module
  • [96] RT6EV6OP delegate update events to drawings
  • [97] R5QXEHUI somebody stop me
  • [98] MUJTM6RE bring back a level of wrapping
  • [99] PX7DDEMO autosave slightly less aggressively
  • [100] 4YDBYBA4 clean up memory leak experiments
  • [101] QCQHLMST always have a filename
  • [102] OAHNWDYG .
  • [103] VG75U7IM bugfix: typing should delete highlighted text
  • [104] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing
  • [105] BYG5CEMV support for naming points
  • [106] L5USRTY2 inline
  • [107] VIU2FBNV make sure to save right when quitting
  • [108] CNCYMM6A make test initializations a little more obvious
  • [109] R6GUSTBY default font size and line-height
  • [110] AOIRVVJA revert selection logic to before commit 3ffc2ed8f
  • [111] CE4LZV4T drop last couple of manual tests
  • [112] VHQCNMAR several more modules
  • [113] 537TQ2QN some more logging
  • [114] 5ZA3BRNY add state arg to a few functions
  • [115] G77XIN7M selecting a stroke
  • [116] EFMLTMZG bugfix: restrict strokes to the drawing they started in
  • [117] NEXUNNCF extract a function
  • [118] JCSLDGAH beginnings of support for multiple shapes
  • [119] XX7G2FFJ intermingle freehand line drawings with text
  • [120] CIQN2MDE bugfix: typing a capital letter deletes selection
  • [121] IZZVOCLB confirm that we have access to all of the love API
  • [122] SPNMXTYR have file API operate on state object
  • [123] ICIIP4DB slightly better default sizing of drawings
  • [124] 5STHSG4U remove some duplication
  • [125] YGCT2D2O start loading settings as applicable
  • [126] 7M7LS7I2 start saving some settings to disk on quit
  • [127] PP2IIHL6 stop putting button state in a global
  • [128] SDRXK4X5 move
  • [129] 32V6ZHQB Merge lines.love
  • [130] W4UVZETR 2 regressions:
  • [131] TLOAPLBJ add a license
  • [132] RF5ALVNY allow the window to be resized
  • [133] YKRF5V3Z starting to load/save
  • [134] AVQ5MC5D finish uppercasing all globals
  • [135] FYS7TCDW bugfix
  • [136] 6DE7RBZ6 move mouse_released events to Drawing
  • [137] EF6MFB46 assume we always have a filename
  • [138] 3QNOKBFM beginnings of a test harness
  • [139] WOXIYUTL bugfix: manage screen_top and cursor when resizing
  • [140] J2SVGR2E experiment: blinking cursor
  • [141] SVJZZDC3 snapshot - no, that's all wrong
  • [142] O2UFJ6G3 switch from freehand to just straight lines
  • [143] IFGAJAF7 add a level of indirection to vertices of shapes
  • [144] 2FBLO5FH adjust window size
  • [145] J5IEBT64 enforce press/release state only processed once
  • [146] JRLBUB6L more intuitive point delete from polygons
  • [147] QCPXQ2E3 add state arg to a few functions
  • [148] M36DBSDE bit more polish to help screen
  • [149] TGZAJUEF bring back a set of constants
  • [150] DJGC4ZEF simplify hysteresis logic
  • [151] H3KWPK3G regression: dropping files on the window
  • [152] HFI2YR2C rip out geometry commandline arg
  • [153] 2Y5GGGJ4 correct a mis-named threshold
  • [154] YTSPVDZH first successful pagedown test, first bug found by test
  • [155] 4QQBMWLL regression: typing uppercase letters in text
  • [156] 3QQZ7W4E bring couple more globals back to the app level
  • [157] EMHRPJ3R no, that's not right
  • [158] LF7BWEG4 group all editor globals
  • [159] 5XQ4Y7NU reorg manual tests doc
  • [160] D2GCFTTT clean up repl functionality
  • [161] RSZD5A7G forgot to add json.lua
  • [162] M6TH7VSZ rip out notion of Line_width
  • [163] VSBSWTE4 bugfix: where cursor is drawn
  • [164] 4KC7I3E2 make colors easier to edit
  • [*] E5FYDACS a likely source of issues
  • [*] D4FEFHQC flesh out Readme

Change contents

  • replacement in text.lua at line 3
    [10.30][10.2774:2775](),[10.2775][10.43921:43955](),[10.43955][10.2775:2790](),[10.2775][10.2775:2790](),[10.2790][10.43956:43977]()
    require 'search'
    require 'select'
    require 'undo'
    require 'text_tests'
    [10.43]
    [10.1567]
    AB_padding = 20 -- space in pixels between A side and B side
  • file addition: source_undo.lua (----------)
    [10.2]
    -- undo/redo by managing the sequence of events in the current session
    -- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu
    -- Incredibly inefficient; we make a copy of lines on every single keystroke.
    -- The hope here is that we're either editing small files or just reading large files.
    -- TODO: highlight stuff inserted by any undo/redo operation
    -- TODO: coalesce multiple similar operations
    function record_undo_event(State, data)
    State.history[State.next_history] = data
    State.next_history = State.next_history+1
    for i=State.next_history,#State.history do
    State.history[i] = nil
    end
    end
    function undo_event(State)
    if State.next_history > 1 then
    --? print('moving to history', State.next_history-1)
    State.next_history = State.next_history-1
    local result = State.history[State.next_history]
    return result
    end
    end
    function redo_event(State)
    if State.next_history <= #State.history then
    --? print('restoring history', State.next_history+1)
    local result = State.history[State.next_history]
    State.next_history = State.next_history+1
    return result
    end
    end
    -- Copy all relevant global state.
    -- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
    function snapshot(State, s,e)
    -- Snapshot everything by default, but subset if requested.
    assert(s)
    if e == nil then
    e = s
    end
    assert(#State.lines > 0)
    if s < 1 then s = 1 end
    if s > #State.lines then s = #State.lines end
    if e < 1 then e = 1 end
    if e > #State.lines then e = #State.lines end
    -- compare with App.initialize_globals
    local event = {
    screen_top=deepcopy(State.screen_top1),
    selection=deepcopy(State.selection1),
    cursor=deepcopy(State.cursor1),
    lines={},
    start_line=s,
    end_line=e,
    -- no filename; undo history is cleared when filename changes
    }
    -- deep copy lines without cached stuff like text fragments
    for i=s,e do
    local line = State.lines[i]
    table.insert(event.lines, {data=line.data, dataB=line.dataB})
    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
  • file addition: source_text_tests.lua (----------)
    [10.2]
    -- major tests for text editing flows
    function test_initial_state()
    io.write('\ntest_initial_state')
    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)
    check_eq(#Editor_state.lines, 1, 'F - test_initial_state/#lines')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_initial_state/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_initial_state/cursor:pos')
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_initial_state/screen_top:line')
    check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos')
    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')
    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()
    Editor_state.lines = load_array{'abc', '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
  • file addition: source_text.lua (----------)
    [10.2]
    -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
    Text = {}
    AB_padding = 20 -- space in pixels between A side and B side
    -- draw a line starting from startpos to screen at y between State.left and State.right
    -- return the final y, and pos,posB of start of final screen line drawn
    function Text.draw(State, line_index, y, startpos, startposB)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    line_cache.starty = y
    line_cache.startpos = startpos
    line_cache.startposB = startposB
    -- draw A side
    local overflows_screen, x, pos, screen_line_starting_pos
    if startpos then
    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)
    if overflows_screen then
    return y, screen_line_starting_pos
    end
    if Focus == 'edit' and State.cursor1.pos then
    if State.search_term == nil then
    if line_index == State.cursor1.line and State.cursor1.pos == pos then
    Text.draw_cursor(State, x, y)
    end
    end
    end
    else
    x = State.left
    end
    -- check for B side
    --? if line_index == 8 then print('checking for B side') end
    if line.dataB == nil then
    assert(y)
    assert(screen_line_starting_pos)
    --? if line_index == 8 then print('return 1') end
    return y, screen_line_starting_pos
    end
    if not State.expanded and not line.expanded then
    assert(y)
    assert(screen_line_starting_pos)
    --? if line_index == 8 then print('return 2') end
    button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},
    icon = function(button_params)
    App.color(Fold_background_color)
    love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)
    end,
    onpress1 = function()
    line.expanded = true
    end,
    })
    return y, screen_line_starting_pos
    end
    -- draw B side
    --? if line_index == 8 then print('drawing B side') end
    App.color(Fold_color)
    --? if Foo then
    --? print('draw:', State.lines[line_index].data, "=====", State.lines[line_index].dataB, 'starting from x', x+AB_padding)
    --? end
    if startposB then
    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)
    else
    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)
    end
    if overflows_screen then
    return y, nil, screen_line_starting_pos
    end
    --? if line_index == 8 then print('a') end
    if Focus == 'edit' and State.cursor1.posB then
    --? if line_index == 8 then print('b') end
    if State.search_term == nil then
    --? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
    if line_index == State.cursor1.line and State.cursor1.posB == pos then
    Text.draw_cursor(State, x, y)
    end
    end
    end
    return y, nil, screen_line_starting_pos
    end
    -- Given an array of fragments, draw the subset starting from pos to screen
    -- starting from (x,y).
    -- Return:
    -- - whether we got to bottom of screen before end of line
    -- - the final (x,y)
    -- - the final pos
    -- - starting pos of the final screen line drawn
    function Text.draw_wrapping_line(State, line_index, x,y, startpos)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    --? print('== line', line_index, '^'..line.data..'$')
    local screen_line_starting_pos = startpos
    Text.compute_fragments(State, line_index)
    local pos = 1
    initialize_color()
    for _, f in ipairs(line_cache.fragments) do
    App.color(Text_color)
    local frag, frag_text = f.data, f.text
    select_color(frag)
    local frag_len = utf8.len(frag)
    --? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
    if pos < startpos then
    -- render nothing
    --? print('skipping', frag)
    else
    -- render fragment
    local frag_width = App.width(frag_text)
    if x + frag_width > State.right then
    assert(x > State.left) -- no overfull lines
    y = y + State.line_height
    if y + State.line_height > App.screen.height then
    return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
    end
    screen_line_starting_pos = pos
    x = State.left
    end
    App.screen.draw(frag_text, x,y)
    -- render cursor if necessary
    if State.cursor1.pos and line_index == State.cursor1.line then
    if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
    if State.search_term then
    if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
    local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term))
    App.color(Text_color)
    love.graphics.print(State.search_term, x+lo_px,y)
    end
    elseif Focus == 'edit' then
    Text.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)
    App.color(Text_color)
    end
    end
    end
    x = x + frag_width
    end
    pos = pos + frag_len
    end
    return false, x,y, pos, screen_line_starting_pos
    end
    function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    local screen_line_starting_pos = startpos
    Text.compute_fragmentsB(State, line_index, x)
    local pos = 1
    for _, f in ipairs(line_cache.fragmentsB) do
    local frag, frag_text = f.data, f.text
    local frag_len = utf8.len(frag)
    --? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
    if pos < startpos then
    -- render nothing
    --? print('skipping', frag)
    else
    -- render fragment
    local frag_width = App.width(frag_text)
    if x + frag_width > State.right then
    assert(x > State.left) -- no overfull lines
    y = y + State.line_height
    if y + State.line_height > App.screen.height then
    return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
    end
    screen_line_starting_pos = pos
    x = State.left
    end
    App.screen.draw(frag_text, x,y)
    -- render cursor if necessary
    if State.cursor1.posB and line_index == State.cursor1.line then
    if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then
    if State.search_term then
    if State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term then
    local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term))
    App.color(Fold_color)
    love.graphics.print(State.search_term, x+lo_px,y)
    end
    elseif Focus == 'edit' then
    Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)
    App.color(Fold_color)
    end
    end
    end
    x = x + frag_width
    end
    pos = pos + frag_len
    end
    return false, x,y, pos, screen_line_starting_pos
    end
    function Text.draw_cursor(State, x, y)
    -- blink every 0.5s
    if math.floor(Cursor_time*2)%2 == 0 then
    App.color(Cursor_color)
    love.graphics.rectangle('fill', x,y, 3,State.line_height)
    end
    State.cursor_x = x
    State.cursor_y = y+State.line_height
    end
    function Text.populate_screen_line_starting_pos(State, line_index)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    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]
    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)
    -- join lines
    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
    State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
    table.remove(State.lines, State.cursor1.line)
    table.remove(State.line_cache, State.cursor1.line)
    State.cursor1.line = State.cursor1.line-1
    end
    if State.screen_top1.line > #State.lines then
    Text.populate_screen_line_starting_pos(State, #State.lines)
    local line_cache = State.line_cache[#State.line_cache]
    State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
    elseif Text.lt1(State.cursor1, State.screen_top1) then
    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
    -- 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
    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)
    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})
    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
    y = y - State.line_height
    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
    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    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.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
    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
    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
    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
    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)
    --? 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')
    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)
    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
    State.screen_top1 = {line=State.cursor1.line, pos=1} -- 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
    elseif State.cursor1.line > 1 then
    State.cursor1 = {
    line = State.cursor1.line-1,
    pos = utf8.len(State.lines[State.cursor1.line-1].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.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)
    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
    elseif State.cursor1.line <= #State.lines-1 then
    State.cursor1 = {line=State.cursor1.line+1, pos=1}
    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
    elseif State.cursor1.line <= #State.lines-1 then
    -- overflow back into A side
    State.cursor1 = {line=State.cursor1.line+1, pos=1}
    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)
    if State.top > 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 h = State.line_height
    if y - h < State.top then
    break
    end
    y = y - h
    top2 = Text.previous_screen_line(State, top2)
    end
    --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
    State.screen_top1 = Text.to1(State, top2)
    --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
    --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Foo = true
    end
    function Text.in_line(State, line_index, x,y)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    if line_cache.starty == nil then return false end -- outside current page
    if y < line_cache.starty then return false end
    local num_screen_lines = 0
    if line_cache.startpos then
    Text.populate_screen_line_starting_pos(State, line_index)
    num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1
    end
    --? print('#screenlines after A', num_screen_lines)
    if line.dataB and (State.expanded or line.expanded) then
    local x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding
    Text.populate_screen_line_starting_posB(State, line_index, x)
    --? print('B:', x, #line_cache.screen_line_starting_posB)
    if line_cache.startposB then
    num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) -- no +1; first screen line of B side overlaps with A side
    else
    num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1) -- no +1; first screen line of B side overlaps with A side
    end
    end
    --? print('#screenlines after B', num_screen_lines)
    return y < line_cache.starty + State.line_height*num_screen_lines
    end
    -- convert mx,my in pixels to schema-1 coordinates
    -- returns: pos, posB
    -- scenarios:
    -- line without B side
    -- line with B side collapsed
    -- line with B side expanded
    -- line starting rendering in A side (startpos ~= nil)
    -- line starting rendering in B side (startposB ~= nil)
    -- my on final screen line of A side
    -- mx to right of A side with no B side
    -- mx to right of A side but left of B side
    -- mx to right of B side
    -- preconditions:
    -- startpos xor startposB
    -- expanded -> dataB
    function Text.to_pos_on_line(State, line_index, mx, my)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    assert(my >= line_cache.starty)
    -- duplicate some logic from Text.draw
    local y = line_cache.starty
    --? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB)
    if line_cache.startpos then
    local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
    for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
    local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
    local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
    --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
    local nexty = y + State.line_height
    if my < nexty then
    -- On all wrapped screen lines but the final one, clicks past end of
    -- line position cursor on final character of screen line.
    -- (The final screen line positions past end of screen line as always.)
    if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
    --? print('past end of non-final line; return')
    return line_cache.screen_line_starting_pos[screen_line_index+1]-1
    end
    local s = string.sub(line.data, screen_line_starting_byte_offset)
    --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
    local screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left)
    if line.dataB == nil then
    -- no B side
    return screen_line_starting_pos + screen_line_posA - 1
    end
    if not State.expanded and not line.expanded then
    -- B side is not expanded
    return screen_line_starting_pos + screen_line_posA - 1
    end
    local lenA = utf8.len(s)
    if screen_line_posA < lenA then
    -- mx is within A side
    return screen_line_starting_pos + screen_line_posA - 1
    end
    local max_xA = State.left+Text.x(s, lenA+1)
    if mx < max_xA + AB_padding then
    -- mx is in the space between A and B side
    return screen_line_starting_pos + screen_line_posA - 1
    end
    mx = mx - max_xA - AB_padding
    local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)
    return nil, screen_line_posB
    end
    y = nexty
    end
    end
    -- look in screen lines composed entirely of the B side
    assert(State.expanded or line.expanded)
    local start_screen_line_indexB
    if line_cache.startposB then
    start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)
    else
    start_screen_line_indexB = 2 -- skip the first line of side B, which we checked above
    end
    for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do
    local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB]
    local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB)
    --? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
    local nexty = y + State.line_height
    if my < nexty then
    -- On all wrapped screen lines but the final one, clicks past end of
    -- line position cursor on final character of screen line.
    -- (The final screen line positions past end of screen line as always.)
    --? print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))
    if screen_line_indexB < #line_cache.screen_line_starting_posB and mx > State.left + Text.screen_line_widthB(State, line_index, screen_line_indexB) then
    --? print('past end of non-final line; return')
    return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1
    end
    local s = string.sub(line.dataB, screen_line_starting_byte_offsetB)
    --? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)
    return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1
    end
    y = nexty
    end
    assert(false)
    end
    function Text.screen_line_width(State, line_index, i)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    local start_pos = line_cache.screen_line_starting_pos[i]
    local start_offset = Text.offset(line.data, start_pos)
    local screen_line
    if i < #line_cache.screen_line_starting_pos then
    local past_end_pos = line_cache.screen_line_starting_pos[i+1]
    local past_end_offset = Text.offset(line.data, past_end_pos)
    screen_line = string.sub(line.data, start_offset, past_end_offset-1)
    else
    screen_line = string.sub(line.data, start_pos)
    end
    local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
    return App.width(screen_line_text)
    end
    function Text.screen_line_widthB(State, line_index, i)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    local start_posB = line_cache.screen_line_starting_posB[i]
    local start_offsetB = Text.offset(line.dataB, start_posB)
    local screen_line
    if i < #line_cache.screen_line_starting_posB then
    --? print('non-final', i)
    local past_end_posB = line_cache.screen_line_starting_posB[i+1]
    local past_end_offsetB = Text.offset(line.dataB, past_end_posB)
    --? print('between', start_offsetB, past_end_offsetB)
    screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1)
    else
    --? print('final', i)
    --? print('after', start_offsetB)
    screen_line = string.sub(line.dataB, start_offsetB)
    end
    local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
    --? local result = App.width(screen_line_text)
    --? print('=>', result)
    --? return result
    return App.width(screen_line_text)
    end
    function Text.screen_line_index(screen_line_starting_pos, pos)
    for i = #screen_line_starting_pos,1,-1 do
    if screen_line_starting_pos[i] <= pos then
    return i
    end
    end
    end
    function Text.screen_line_indexB(screen_line_starting_posB, posB)
    if posB == nil then
    return 0
    end
    assert(screen_line_starting_posB)
    for i = #screen_line_starting_posB,1,-1 do
    if screen_line_starting_posB[i] <= posB then
    return i
    end
    end
    end
    -- convert x pixel coordinate to pos
    -- oblivious to wrapping
    -- result: 1 to len+1
    function Text.nearest_cursor_pos(line, x, left)
    if x < left then
    return 1
    end
    local len = utf8.len(line)
    local max_x = left+Text.x(line, len+1)
    if x > max_x then
    return len+1
    end
    local leftpos, rightpos = 1, len+1
    --? print('-- nearest', x)
    while true do
    --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
    if leftpos == rightpos then
    return leftpos
    end
    local curr = math.floor((leftpos+rightpos)/2)
    local currxmin = left+Text.x(line, curr)
    local currxmax = left+Text.x(line, curr+1)
    --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
    if currxmin <= x and x < currxmax then
    if x-currxmin < currxmax-x then
    return curr
    else
    return curr+1
    end
    end
    if leftpos >= rightpos-1 then
    return rightpos
    end
    if currxmin > x then
    rightpos = curr
    else
    leftpos = curr
    end
    end
    assert(false)
    end
    -- return the nearest index of line (in utf8 code points) which lies entirely
    -- within x pixels of the left margin
    -- result: 0 to len+1
    function Text.nearest_pos_less_than(line, x)
    --? print('', '-- nearest_pos_less_than', line, x)
    local len = utf8.len(line)
    local max_x = Text.x_after(line, len)
    if x > max_x then
    return len+1
    end
    local left, right = 0, len+1
    while true do
    local curr = math.floor((left+right)/2)
    local currxmin = Text.x_after(line, curr+1)
    local currxmax = Text.x_after(line, curr+2)
    --? print('', x, left, right, curr, currxmin, currxmax)
    if currxmin <= x and x < currxmax then
    return curr
    end
    if left >= right-1 then
    return left
    end
    if currxmin > x then
    right = curr
    else
    left = curr
    end
    end
    assert(false)
    end
    function Text.x_after(s, pos)
    local offset = Text.offset(s, math.min(pos+1, #s+1))
    local s_before = s:sub(1, offset-1)
    --? print('^'..s_before..'$')
    local text_before = App.newText(love.graphics.getFont(), s_before)
    return App.width(text_before)
    end
    function Text.x(s, pos)
    local offset = Text.offset(s, pos)
    local s_before = s:sub(1, offset-1)
    local text_before = App.newText(love.graphics.getFont(), s_before)
    return App.width(text_before)
    end
    function Text.to2(State, loc1)
    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
    --? print('a')
    return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
    elseif loc2.line == 1 then
    --? print('b')
    return loc2
    else
    Text.populate_screen_line_starting_pos(State, loc2.line-1)
    if State.lines[loc2.line-1].dataB == nil or
    (not State.expanded and not State.lines[loc2.line-1].expanded) then
    --? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)
    return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
    end
    -- try to switch to B
    local prev_line_cache = State.line_cache[loc2.line-1]
    local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding
    Text.populate_screen_line_starting_posB(State, loc2.line-1, x)
    local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB
    --? print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x)
    if #screen_line_starting_posB > 1 then
    --? print('c2')
    return {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1}
    else
    --? print('c3')
    -- if there's only one screen line, assume it overlaps with A, so remain in A
    return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
    end
    end
    end
    function Text.previous_screen_lineB(State, loc2)
    if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A side
    return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1}
    else
    -- switch to A side
    -- TODO: handle case where fold lands precisely at end of a new screen-line
    return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}
    end
    end
    -- resize helper
    function Text.tweak_screen_top_and_cursor(State)
    if State.screen_top1.pos == 1 then return end
    Text.populate_screen_line_starting_pos(State, State.screen_top1.line)
    local line = State.lines[State.screen_top1.line]
    local line_cache = State.line_cache[State.screen_top1.line]
    for i=2,#line_cache.screen_line_starting_pos do
    local pos = line_cache.screen_line_starting_pos[i]
    if pos == State.screen_top1.pos then
    break
    end
    if pos > State.screen_top1.pos then
    -- make sure screen top is at start of a screen line
    local prev = line_cache.screen_line_starting_pos[i-1]
    if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
    State.screen_top1.pos = prev
    else
    State.screen_top1.pos = pos
    end
    break
    end
    end
    -- make sure cursor is on screen
    if Text.lt1(State.cursor1, State.screen_top1) then
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
    elseif State.cursor1.line >= State.screen_bottom1.line then
    --? print('too low')
    if Text.cursor_out_of_screen(State) then
    --? print('tweak')
    local pos,posB = Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5)
    State.cursor1 = {line=State.screen_bottom1.line, pos=pos, posB=posB}
    end
    end
    end
    -- slightly expensive since it redraws the screen
    function Text.cursor_out_of_screen(State)
    App.draw()
    return State.cursor_y == nil
    -- this approach is cheaper and almost works, except on the final screen
    -- where file ends above bottom of screen
    --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
    --? local botline1 = {line=State.cursor1.line, pos=botpos}
    --? return Text.lt1(State.screen_bottom1, botline1)
    end
    function Text.redraw_all(State)
    --? print('clearing fragments')
    State.line_cache = {}
    for i=1,#State.lines do
    State.line_cache[i] = {}
    end
    end
    function Text.clear_screen_line_cache(State, line_index)
    State.line_cache[line_index].fragments = nil
    State.line_cache[line_index].fragmentsB = nil
    State.line_cache[line_index].screen_line_starting_pos = nil
    State.line_cache[line_index].screen_line_starting_posB = nil
    end
    function trim(s)
    return s:gsub('^%s+', ''):gsub('%s+$', '')
    end
    function ltrim(s)
    return s:gsub('^%s+', '')
    end
    function rtrim(s)
    return s:gsub('%s+$', '')
    end
  • file addition: source_tests.lua (----------)
    [10.2]
    function test_resize_window()
    io.write('\ntest_resize_window')
    App.screen.init{width=300, height=300}
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Log_browser_state = edit.initialize_test_state()
    check_eq(App.screen.width, 300, 'F - test_resize_window/baseline/width')
    check_eq(App.screen.height, 300, 'F - test_resize_window/baseline/height')
    check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/baseline/left_margin')
    App.resize(200, 400)
    check_eq(App.screen.width, 200, 'F - test_resize_window/width')
    check_eq(App.screen.height, 400, 'F - test_resize_window/height')
    check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/left_margin')
    -- ugly; right margin switches from 0 after resize
    check_eq(Editor_state.right, 200-Margin_right, 'F - test_resize_window/right_margin')
    check_eq(Editor_state.width, 200-Test_margin_left-Margin_right, 'F - test_resize_window/drawing_width')
    -- TODO: how to make assertions about when App.update got past the early exit?
    end
    function test_drop_file()
    io.write('\ntest_drop_file')
    App.screen.init{width=Editor_state.left+300, height=300}
    Editor_state = edit.initialize_test_state()
    App.filesystem['foo'] = 'abc\ndef\nghi\n'
    local fake_dropped_file = {
    opened = false,
    getFilename = function(self)
    return 'foo'
    end,
    open = function(self)
    self.opened = true
    end,
    lines = function(self)
    assert(self.opened)
    return App.filesystem['foo']:gmatch('[^\n]+')
    end,
    close = function(self)
    self.opened = false
    end,
    }
    App.filedropped(fake_dropped_file)
    check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines')
    check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1')
    check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2')
    check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')
    edit.draw(Editor_state)
    end
    function test_drop_file_saves_previous()
    io.write('\ntest_drop_file_saves_previous')
    App.screen.init{width=Editor_state.left+300, height=300}
    -- initially editing a file called foo that hasn't been saved to filesystem yet
    Editor_state.lines = load_array{'abc', 'def'}
    Editor_state.filename = 'foo'
    schedule_save(Editor_state)
    -- now drag a new file bar from the filesystem
    App.filesystem['bar'] = 'abc\ndef\nghi\n'
    local fake_dropped_file = {
    opened = false,
    getFilename = function(self)
    return 'bar'
    end,
    open = function(self)
    self.opened = true
    end,
    lines = function(self)
    assert(self.opened)
    return App.filesystem['bar']:gmatch('[^\n]+')
    end,
    close = function(self)
    self.opened = false
    end,
    }
    App.filedropped(fake_dropped_file)
    -- filesystem now contains a file called foo
    check_eq(App.filesystem['foo'], 'abc\ndef\n', 'F - test_drop_file_saves_previous')
    end
  • file addition: source_file.lua (----------)
    [10.2]
    -- primitives for saving to file and loading from file
    Fold = '\x1e' -- ASCII RS (record separator)
    function file_exists(filename)
    local infile = App.open_for_reading(filename)
    if infile then
    infile:close()
    return true
    else
    return false
    end
    end
    function load_from_disk(State)
    local infile = App.open_for_reading(State.filename)
    State.lines = load_from_file(infile)
    if infile then infile:close() end
    end
    function load_from_file(infile)
    local result = {}
    if infile then
    local infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)
    while true do
    local line = infile_next_line()
    if line == nil then break end
    local line_info = {}
    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
    end
    if #result == 0 then
    table.insert(result, {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
    outfile:write(line.data)
    if line.dataB and #line.dataB > 0 then
    outfile:write(Fold)
    outfile:write(line.dataB)
    end
    outfile:write('\n')
    end
    outfile:close()
    end
    function file_exists(filename)
    local infile = App.open_for_reading(filename)
    if infile then
    infile:close()
    return true
    else
    return false
    end
    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
    local line_info = {}
    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 #result == 0 then
    table.insert(result, {data=''})
    end
    return result
    end
  • file addition: source_edit.lua (----------)
    [10.2]
    -- some constants people might like to tweak
    Text_color = {r=0, g=0, b=0}
    Cursor_color = {r=1, g=0, b=0}
    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
    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
    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 = {
    -- 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
    -- 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,
    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
    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]
    --? print('draw:', y, line_index, line)
    if y + State.line_height > App.screen.height then break end
    State.screen_bottom1 = {line=line_index, pos=nil, posB=nil}
    --? print('text.draw', y, line_index)
    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
    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)
    end
    if State.search_term then
    Text.draw_search_bar(State)
    end
    end
    function edit.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
    --? print('press', State.selection1.line, State.selection1.pos)
    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
    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
    end
    end
    function edit.mouse_released(State, x,y, mouse_button)
    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)
    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
    elseif chord == 'C-d' 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)
    -- 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)
    -- 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)})
    -- dispatch to text
    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
  • file addition: source.lua (----------)
    [10.2]
    source = {}
    Editor_state = {}
    -- called both in tests and real run
    function source.initialize_globals()
    -- tests currently mostly clear their own state
    Show_log_browser_side = false
    Focus = 'edit'
    Show_file_navigator = false
    File_navigation = {
    candidates = {
    'run',
    'run_tests',
    'log',
    'edit',
    'text',
    'search',
    'select',
    'undo',
    'text_tests',
    '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
    end
    -- environment for a mutable file of bifolded text
    -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
    function source.initialize_edit_side(arg)
    if #arg > 0 then
    Editor_state.filename = arg[1]
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.cursor1 = {line=1, pos=1}
    else
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    end
    if #arg > 1 then
    print('ignoring commandline args after '..arg[1])
    end
    -- We currently start out with side B collapsed.
    -- Other options:
    -- * save all expanded state by line
    -- * expand all if any location is in side B
    if Editor_state.cursor1.line > #Editor_state.lines then
    Editor_state.cursor1 = {line=1, pos=1}
    end
    if Editor_state.screen_top1.line > #Editor_state.lines then
    Editor_state.screen_top1 = {line=1, pos=1}
    end
    edit.eradicate_locations_after_the_fold(Editor_state)
    if rawget(_G, 'jit') then
    jit.off()
    jit.flush()
    end
    end
    function source.load_settings()
    local settings = Settings.source
    love.graphics.setFont(love.graphics.newFont(settings.font_height))
    -- maximize window to determine maximum allowable dimensions
    love.window.setMode(0, 0) -- maximize
    Display_width, Display_height, App.screen.flags = love.window.getMode()
    -- set up desired window dimensions
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(Display_width, 200)
    App.screen.flags.minheight = math.min(Display_height, 200)
    App.screen.width, App.screen.height = settings.width, settings.height
    --? print('setting window from settings:', App.screen.width, App.screen.height)
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    --? print('loading source position', settings.x, settings.y, settings.displayindex)
    source.set_window_position_from_settings(settings)
    Show_log_browser_side = settings.show_log_browser_side
    local right = App.screen.width - Margin_right
    if Show_log_browser_side then
    right = App.screen.width/2 - Margin_right
    end
    Editor_state = edit.initialize_state(Margin_top, Margin_left, right, settings.font_height, math.floor(settings.font_height*1.3))
    Editor_state.filename = settings.filename
    Editor_state.screen_top1 = settings.screen_top
    Editor_state.cursor1 = settings.cursor
    end
    function source.set_window_position_from_settings(settings)
    -- setPosition doesn't quite seem to do what is asked of it on Linux.
    love.window.setPosition(settings.x, settings.y-37, settings.displayindex)
    end
    function source.initialize_default_settings()
    local font_height = 20
    love.graphics.setFont(love.graphics.newFont(font_height))
    local em = App.newText(love.graphics.getFont(), 'm')
    source.initialize_window_geometry(App.width(em))
    Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
    Editor_state.font_height = font_height
    Editor_state.line_height = math.floor(font_height*1.3)
    Editor_state.em = em
    end
    function source.initialize_window_geometry(em_width)
    -- maximize window
    love.window.setMode(0, 0) -- maximize
    Display_width, Display_height, App.screen.flags = love.window.getMode()
    -- shrink height slightly to account for window decoration
    App.screen.height = Display_height-100
    App.screen.width = 40*em_width
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(App.screen.width, 200)
    App.screen.flags.minheight = math.min(App.screen.width, 200)
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    print('initializing source position')
    if Settings == nil then Settings = {} end
    if Settings.source == nil then Settings.source = {} end
    Settings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()
    end
    function source.resize(w, h)
    --? print(("Window resized to width: %d and height: %d."):format(w, h))
    App.screen.width, App.screen.height = w, h
    Text.redraw_all(Editor_state)
    Editor_state.selection1 = {} -- no support for shift drag while we're resizing
    if Show_log_browser_side then
    Editor_state.right = App.screen.width/2 - Margin_right
    else
    Editor_state.right = App.screen.width-Margin_right
    end
    Log_browser_state.left = App.screen.width/2 + Margin_right
    Log_browser_state.right = App.screen.width-Margin_right
    Editor_state.width = Editor_state.right-Editor_state.left
    Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
    --? print('end resize')
    end
    function source.filedropped(file)
    -- first make sure to save edits on any existing file
    if Editor_state.next_save then
    save_to_disk(Editor_state)
    end
    -- clear the slate for the new file
    Editor_state.filename = file:getFilename()
    file:open('r')
    Editor_state.lines = load_from_file(file)
    file:close()
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.cursor1 = {line=1, pos=1}
    end
    -- a copy of source.filedropped when given a filename
    function source.switch_to_file(filename)
    -- first make sure to save edits on any existing file
    if Editor_state.next_save then
    save_to_disk(Editor_state)
    end
    -- clear the slate for the new file
    Editor_state.filename = filename
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.cursor1 = {line=1, pos=1}
    end
    function source.draw()
    source.draw_menu_bar()
    edit.draw(Editor_state)
    if Show_log_browser_side then
    -- divider
    App.color(Divider_color)
    love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)
    --
    log_browser.draw(Log_browser_state)
    end
    end
    function source.update(dt)
    Cursor_time = Cursor_time + dt
    if App.mouse_x() < Editor_state.right then
    edit.update(Editor_state, dt)
    elseif Show_log_browser_side then
    log_browser.update(Log_browser_state, dt)
    end
    end
    function source.quit()
    edit.quit(Editor_state)
    log_browser.quit(Log_browser_state)
    -- convert any bifold files here
    end
    function source.convert_bifold_text(infilename, outfilename)
    local contents = love.filesystem.read(infilename)
    contents = contents:gsub('\u{1e}', ';')
    love.filesystem.write(outfilename, contents)
    end
    function source.settings()
    if Current_app == 'source' then
    --? print('reading source window position')
    Settings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()
    end
    local filename = Editor_state.filename
    if filename:sub(1,1) ~= '/' then
    filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
    end
    --? print('saving source settings', Settings.source.x, Settings.source.y, Settings.source.displayindex)
    return {
    x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,
    width=App.screen.width, height=App.screen.height,
    font_height=Editor_state.font_height,
    filename=filename,
    screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1,
    show_log_browser_side=Show_log_browser_side,
    focus=Focus,
    }
    end
    function source.mouse_pressed(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    --? print('mouse click', x, y)
    --? print(Editor_state.left, Editor_state.right)
    --? print(Log_browser_state.left, Log_browser_state.right)
    if Editor_state.left <= x and x < Editor_state.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
  • replacement in search.lua at line 33
    [10.219][10.219:282]()
    State.cursor1.line = i
    State.cursor1.pos = pos
    [10.219]
    [10.282]
    State.cursor1 = {line=i, pos=pos}
  • replacement in search.lua at line 43
    [10.50207][10.425:488]()
    State.cursor1.line = i
    State.cursor1.pos = pos
    [10.50207]
    [10.50258]
    State.cursor1 = {line=i, pos=pos}
  • replacement in search.lua at line 79
    [10.51016][10.1297:1360]()
    State.cursor1.line = i
    State.cursor1.pos = pos
    [10.51016]
    [10.51067]
    State.cursor1 = {line=i, pos=pos}
  • replacement in search.lua at line 89
    [10.51247][10.1470:1533]()
    State.cursor1.line = i
    State.cursor1.pos = pos
    [10.51247]
    [10.51298]
    State.cursor1 = {line=i, pos=pos}
  • replacement in search.lua at line 114
    [10.507][10.507:532]()
    function find(s, pat, i)
    [10.507]
    [10.532]
    function find(s, pat, i, plain)
  • replacement in search.lua at line 116
    [10.562][10.562:586]()
    return s:find(pat, i)
    [10.562]
    [10.51762]
    return s:find(pat, i, plain)
  • replacement in search.lua at line 119
    [10.51767][10.51767:51793]()
    function rfind(s, pat, i)
    [10.51767]
    [10.587]
    function rfind(s, pat, i, plain)
  • replacement in search.lua at line 125
    [10.51901][10.51901:51937]()
    local rendpos = rs:find(rpat, ri)
    [10.51901]
    [10.51937]
    local rendpos = rs:find(rpat, ri, plain)
  • file move: main_tests.lua (----------)run_tests.lua (----------)
    [10.2]
    [10.7]
  • file addition: run.lua (----------)
    [10.2]
    run = {}
    Editor_state = {}
    -- called both in tests and real run
    function run.initialize_globals()
    -- tests currently mostly clear their own state
    -- 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 run.initialize(arg)
    love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
    love.keyboard.setKeyRepeat(true)
    love.graphics.setBackgroundColor(1,1,1)
    if Settings then
    run.load_settings()
    else
    run.initialize_default_settings()
    end
    if #arg > 0 then
    Editor_state.filename = arg[1]
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.cursor1 = {line=1, pos=1}
    else
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    end
    love.window.setTitle('text.love - '..Editor_state.filename)
    if #arg > 1 then
    print('ignoring commandline args after '..arg[1])
    end
    if rawget(_G, 'jit') then
    jit.off()
    jit.flush()
    end
    end
    function run.load_settings()
    love.graphics.setFont(love.graphics.newFont(Settings.font_height))
    -- maximize window to determine maximum allowable dimensions
    App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
    -- set up desired window dimensions
    love.window.setPosition(Settings.x, Settings.y, Settings.displayindex)
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(App.screen.width, 200)
    App.screen.flags.minheight = math.min(App.screen.width, 200)
    App.screen.width, App.screen.height = Settings.width, Settings.height
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, Settings.font_height, math.floor(Settings.font_height*1.3))
    Editor_state.filename = Settings.filename
    Editor_state.screen_top1 = Settings.screen_top
    Editor_state.cursor1 = Settings.cursor
    end
    function run.initialize_default_settings()
    local font_height = 20
    love.graphics.setFont(love.graphics.newFont(font_height))
    local em = App.newText(love.graphics.getFont(), 'm')
    run.initialize_window_geometry(App.width(em))
    Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
    Editor_state.font_height = font_height
    Editor_state.line_height = math.floor(font_height*1.3)
    Editor_state.em = em
    Settings = run.settings()
    end
    function run.initialize_window_geometry(em_width)
    -- maximize window
    love.window.setMode(0, 0) -- maximize
    App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
    -- shrink height slightly to account for window decoration
    App.screen.height = App.screen.height-100
    App.screen.width = 40*em_width
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(App.screen.width, 200)
    App.screen.flags.minheight = math.min(App.screen.width, 200)
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    end
    function run.resize(w, h)
    --? print(("Window resized to width: %d and height: %d."):format(w, h))
    App.screen.width, App.screen.height = w, h
    Text.redraw_all(Editor_state)
    Editor_state.selection1 = {} -- no support for shift drag while we're resizing
    Editor_state.right = App.screen.width-Margin_right
    Editor_state.width = Editor_state.right-Editor_state.left
    Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
    end
    function run.filedropped(file)
    -- first make sure to save edits on any existing file
    if Editor_state.next_save then
    save_to_disk(Editor_state)
    end
    -- clear the slate for the new file
    App.initialize_globals()
    Editor_state.filename = file:getFilename()
    file:open('r')
    Editor_state.lines = load_from_file(file)
    file:close()
    Text.redraw_all(Editor_state)
    love.window.setTitle('text.love - '..Editor_state.filename)
    end
    function run.draw()
    edit.draw(Editor_state)
    end
    function run.update(dt)
    Cursor_time = Cursor_time + dt
    edit.update(Editor_state, dt)
    end
    function run.quit()
    edit.quit(Editor_state)
    end
    function run.settings()
    local x,y,displayindex = love.window.getPosition()
    local filename = Editor_state.filename
    if filename:sub(1,1) ~= '/' then
    filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
    end
    return {
    x=x, y=y, displayindex=displayindex,
    width=App.screen.width, height=App.screen.height,
    font_height=Editor_state.font_height,
    filename=filename,
    screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1
    }
    end
    function run.mouse_pressed(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.mouse_pressed(Editor_state, x,y, mouse_button)
    end
    function run.mouse_released(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.mouse_released(Editor_state, x,y, mouse_button)
    end
    function run.textinput(t)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.textinput(Editor_state, t)
    end
    function run.keychord_pressed(chord, key)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.keychord_pressed(Editor_state, chord, key)
    end
    function run.key_released(key, scancode)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.key_released(Editor_state, key, scancode)
    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
  • edit in main.lua at line 1
    [10.2]
    [10.8]
    -- Entrypoint for the app. You can edit this file from within the app if
    -- you're careful.
    -- files that come with LÖVE; we can't edit those from within the app
  • edit in main.lua at line 6
    [10.30][10.9281:9303]()
    json = require 'json'
  • replacement in main.lua at line 7
    [10.4][10.4:18](),[10.18][10.748:763]()
    require 'app'
    require 'test'
    [10.4]
    [10.8]
    function load_file_from_source_or_save_directory(filename)
    local contents = love.filesystem.read(filename)
    local code, err = loadstring(contents, filename)
    if code == nil then
    error(err)
    end
    return code()
    end
  • replacement in main.lua at line 16
    [10.9][10.2:21](),[10.19][10.2:21](),[10.30][10.2:21](),[10.2][10.2:21](),[10.21][10.10:27]()
    require 'keychord'
    require 'button'
    [10.9]
    [10.1165]
    json = load_file_from_source_or_save_directory('json.lua')
  • replacement in main.lua at line 18
    [10.1166][10.1166:1187]()
    require 'main_tests'
    [10.1166]
    [10.8]
    load_file_from_source_or_save_directory('app.lua')
    load_file_from_source_or_save_directory('test.lua')
  • replacement in main.lua at line 21
    [10.9][10.9:104](),[10.104][10.28:46](),[10.30][10.21:22](),[10.46][10.21:22](),[10.65][10.21:22](),[10.78][10.21:22](),[10.104][10.21:22](),[10.974][10.21:22](),[10.1187][10.21:22](),[10.21][10.21:22](),[10.22][3.8:45](),[3.45][10.1532:1566](),[10.1532][10.1532:1566](),[10.1566][10.17263:17313](),[10.17313][9.8:100](),[9.100][10.36:37](),[10.17313][10.36:37](),[10.85309][10.36:37](),[10.36][10.36:37](),[10.43][10.75:114](),[10.75][10.75:114]()
    -- delegate most business logic to a layer that can be reused by other projects
    require 'edit'
    Editor_state = {}
    -- called both in tests and real run
    function App.initialize_globals()
    -- tests currently mostly clear their own state
    -- a few text objects we can avoid recomputing unless the font changes
    Text_cache = {}
    -- blinking cursor
    Cursor_time = 0
    [10.9]
    [10.8]
    load_file_from_source_or_save_directory('keychord.lua')
    load_file_from_source_or_save_directory('button.lua')
  • replacement in main.lua at line 24
    [10.9][4.8:44](),[4.44][10.29:64](),[10.29][10.29:64](),[10.64][4.45:132](),[10.64][10.140:144](),[10.114][10.140:144](),[4.132][10.140:144](),[10.140][10.140:144]()
    -- for hysteresis in a few places
    Last_resize_time = App.getTime()
    Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700
    end
    [10.9]
    [10.162]
    -- both sides require (different parts of) the logging framework
    load_file_from_source_or_save_directory('log.lua')
  • replacement in main.lua at line 27
    [10.1600][3.46:74](),[3.74][10.1600:1738](),[10.1600][10.1600:1738](),[10.1738][10.8:50](),[10.19][10.47:48](),[10.21][10.47:48](),[10.24][10.47:48](),[10.39][10.47:48](),[10.39][10.47:48](),[10.39][10.47:48](),[10.50][10.47:48](),[10.63][10.47:48](),[10.175][10.47:48](),[10.248][10.47:48](),[10.330][10.47:48](),[10.365][10.47:48](),[10.1607][10.47:48](),[10.2183][10.47:48](),[10.2253][10.47:48](),[10.47][10.47:48]()
    -- called only for real run
    function App.initialize(arg)
    love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
    love.keyboard.setKeyRepeat(true)
    love.graphics.setBackgroundColor(1,1,1)
    [10.163]
    [10.51]
    -- but some files we want to only load sometimes
    function App.load()
  • replacement in main.lua at line 30
    [10.95][10.95:122](),[10.122][10.17314:17348]()
    load_settings()
    else
    initialize_default_settings()
    [10.95]
    [10.142]
    Settings = json.decode(love.filesystem.read('config'))
    Current_app = Settings.current_app
  • replacement in main.lua at line 34
    [10.33][10.44:63](),[10.38][10.44:63](),[10.75][10.44:63](),[10.63][10.85310:85345](),[10.85345][10.8:41](),[10.41][10.7629:7663](),[10.85408][10.7629:7663](),[10.7663][10.85408:85498](),[10.85408][10.85408:85498](),[10.44][10.376:383](),[10.376][10.376:383](),[10.383][10.42:75](),[10.75][10.7664:7698](),[10.85649][10.7664:7698]()
    if #arg > 0 then
    Editor_state.filename = arg[1]
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.cursor1 = {line=1, pos=1}
    else
    load_from_disk(Editor_state)
    Text.redraw_all(Editor_state)
    [10.127]
    [10.460]
    if Current_app == nil then
    Current_app = 'run'
  • edit in main.lua at line 37
    [10.466][10.8:70]()
    love.window.setTitle('text.love - '..Editor_state.filename)
  • replacement in main.lua at line 38
    [10.125][10.9:82](),[10.9][10.9:82]()
    if #arg > 1 then
    print('ignoring commandline args after '..arg[1])
    [10.9]
    [10.82]
    if Current_app == 'run' then
    load_file_from_source_or_save_directory('file.lua')
    load_file_from_source_or_save_directory('run.lua')
    load_file_from_source_or_save_directory('edit.lua')
    load_file_from_source_or_save_directory('text.lua')
    load_file_from_source_or_save_directory('search.lua')
    load_file_from_source_or_save_directory('select.lua')
    load_file_from_source_or_save_directory('undo.lua')
    load_file_from_source_or_save_directory('text_tests.lua')
    load_file_from_source_or_save_directory('run_tests.lua')
    else
    load_file_from_source_or_save_directory('source_file.lua')
    load_file_from_source_or_save_directory('source.lua')
    load_file_from_source_or_save_directory('commands.lua')
    load_file_from_source_or_save_directory('source_edit.lua')
    load_file_from_source_or_save_directory('log_browser.lua')
    load_file_from_source_or_save_directory('source_text.lua')
    load_file_from_source_or_save_directory('search.lua')
    load_file_from_source_or_save_directory('select.lua')
    load_file_from_source_or_save_directory('source_undo.lua')
    load_file_from_source_or_save_directory('colorize.lua')
    load_file_from_source_or_save_directory('source_text_tests.lua')
    load_file_from_source_or_save_directory('source_tests.lua')
  • edit in main.lua at line 62
    [10.88][10.8:73]()
    if rawget(_G, 'jit') then
    jit.off()
    jit.flush()
    end
  • replacement in main.lua at line 64
    [10.611][10.611:636](),[10.636][10.4762:4894](),[10.4894][10.8:71](),[10.636][10.8:71](),[10.112][10.112:192](),[10.192][10.4895:4933](),[10.4933][10.699:772](),[10.699][10.699:772](),[10.278][10.278:439](),[10.439][10.772:844](),[10.772][10.772:844](),[10.844][10.440:517](),[10.517][10.4934:5089](),[10.5089][10.85929:85973](),[10.17444][10.85929:85973](),[10.517][10.85929:85973](),[10.17700][10.85974:86064](),[10.983][10.85974:86064](),[10.86064][10.1047:1051](),[10.1047][10.1047:1051]()
    function load_settings()
    local settings = json.decode(love.filesystem.read('config'))
    love.graphics.setFont(love.graphics.newFont(settings.font_height))
    -- maximize window to determine maximum allowable dimensions
    App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
    -- set up desired window dimensions
    love.window.setPosition(settings.x, settings.y, settings.displayindex)
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(App.screen.width, 200)
    App.screen.flags.minheight = math.min(App.screen.width, 200)
    App.screen.width, App.screen.height = settings.width, settings.height
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, settings.font_height, math.floor(settings.font_height*1.3))
    Editor_state.filename = settings.filename
    Editor_state.screen_top1 = settings.screen_top
    Editor_state.cursor1 = settings.cursor
    end
    [10.611]
    [10.1051]
    function App.initialize_globals()
    if Current_app == 'run' then
    run.initialize_globals()
    elseif Current_app == 'source' then
    source.initialize_globals()
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 73
    [10.1052][10.17701:18140]()
    function initialize_default_settings()
    local font_height = 20
    love.graphics.setFont(love.graphics.newFont(font_height))
    local em = App.newText(love.graphics.getFont(), 'm')
    initialize_window_geometry(App.width(em))
    Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
    Editor_state.font_height = font_height
    Editor_state.line_height = math.floor(font_height*1.3)
    Editor_state.em = em
    [10.1052]
    [10.1139]
    -- for hysteresis in a few places
    Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700
    Last_resize_time = App.getTime()
  • replacement in main.lua at line 78
    [10.1320][10.18141:18187](),[10.18187][10.78:220](),[10.78][10.78:220](),[10.220][10.18188:18249](),[10.110][10.316:360](),[10.18249][10.316:360](),[10.86116][10.316:360](),[10.316][10.316:360](),[10.360][10.18250:18283](),[10.360][10.384:545](),[10.683][10.384:545](),[10.18283][10.384:545](),[10.384][10.384:545](),[10.545][10.518:595]()
    function initialize_window_geometry(em_width)
    -- maximize window
    love.window.setMode(0, 0) -- maximize
    App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
    -- shrink height slightly to account for window decoration
    App.screen.height = App.screen.height-100
    App.screen.width = 40*em_width
    App.screen.flags.resizable = true
    App.screen.flags.minwidth = math.min(App.screen.width, 200)
    App.screen.flags.minheight = math.min(App.screen.width, 200)
    love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
    [10.1320]
    [10.625]
    function App.initialize(arg)
    if Current_app == 'run' then
    run.initialize(arg)
    elseif Current_app == 'source' then
    source.initialize(arg)
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
    love.window.setTitle('text.love - '..Current_app)
  • replacement in main.lua at line 89
    [10.41][10.1188:1214](),[10.1214][10.358:477](),[10.358][10.358:477](),[10.477][10.6094:6126](),[10.6126][10.86117:86199](),[10.217][10.86117:86199](),[10.86199][10.592:705](),[10.705][10.18284:18372](),[10.86199][10.18284:18372]()
    function App.resize(w, h)
    --? print(("Window resized to width: %d and height: %d."):format(w, h))
    App.screen.width, App.screen.height = w, h
    Text.redraw_all(Editor_state)
    Editor_state.selection1 = {} -- no support for shift drag while we're resizing
    Editor_state.right = App.screen.width-Margin_right
    Editor_state.width = Editor_state.right-Editor_state.left
    Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
    [10.41]
    [10.154]
    function App.resize(w,h)
    if Current_app == 'run' then
    if run.resize then run.resize(w,h) end
    elseif Current_app == 'source' then
    if source.resize then source.resize(w,h) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 101
    [10.1352][10.1003:1059](),[10.1059][10.86541:86574](),[10.86574][10.76:107]()
    -- first make sure to save edits on any existing file
    if Editor_state.next_save then
    save_to_disk(Editor_state)
    [10.1352]
    [10.1113]
    if Current_app == 'run' then
    if run.filedropped then run.filedropped(file) end
    elseif Current_app == 'source' then
    if source.filedropped then source.filedropped(file) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
    love.window.setTitle('text.love - '..Current_app)
    end
    function App.focus(in_focus)
    if in_focus then
    Last_focus_time = App.getTime()
  • replacement in main.lua at line 115
    [10.1119][10.1119:1157](),[10.1157][7.8:35](),[7.35][10.86635:86680](),[10.8235][10.86635:86680](),[10.779][10.71:88](),[10.86680][10.71:88](),[10.71][10.71:88](),[10.88][10.86681:86725](),[10.177][10.1:16](),[10.86725][10.1:16](),[10.119][10.1:16](),[10.16][6.10:42](),[6.42][10.71:133](),[10.16][10.71:133]()
    -- clear the slate for the new file
    App.initialize_globals()
    Editor_state.filename = file:getFilename()
    file:open('r')
    Editor_state.lines = load_from_file(file)
    file:close()
    Text.redraw_all(Editor_state)
    love.window.setTitle('text.love - '..Editor_state.filename)
    [10.1119]
    [6.44]
    if Current_app == 'run' then
    if run.focus then run.focus(in_focus) end
    elseif Current_app == 'source' then
    if source.focus then source.focus(in_focus) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 125
    [10.70][10.2325:2351]()
    edit.draw(Editor_state)
    [10.1373]
    [10.416]
    if Current_app == 'run' then
    run.draw()
    elseif Current_app == 'source' then
    source.draw()
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • edit in main.lua at line 135
    [10.1398][10.115:148]()
    Cursor_time = Cursor_time + dt
  • replacement in main.lua at line 139
    [10.327][10.2352:2384]()
    edit.update(Editor_state, dt)
    [10.327]
    [10.3]
    --
    if Current_app == 'run' then
    run.update(dt)
    elseif Current_app == 'source' then
    source.update(dt)
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 149
    [10.45][10.45:66](),[10.66][10.2385:2411](),[10.193][10.48:135](),[10.2411][10.48:135](),[10.68][10.48:135](),[10.135][10.86892:86933](),[10.86933][10.36:175](),[10.36][10.36:175]()
    function love.quit()
    edit.quit(Editor_state)
    -- save some important settings
    local x,y,displayindex = love.window.getPosition()
    local filename = Editor_state.filename
    if filename:sub(1,1) ~= '/' then
    filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
    [10.8]
    [10.175]
    function App.keychord_pressed(chord, key)
    -- ignore events for some time after window in focus (mostly alt-tab)
    if App.getTime() < Last_focus_time + 0.01 then
    return
  • replacement in main.lua at line 154
    [10.181][10.135:251](),[10.135][10.135:251](),[10.251][10.86934:86976](),[10.86976][10.182:205](),[10.37][10.182:205](),[10.205][10.86977:87047](),[10.184][10.339:396](),[10.87047][10.339:396](),[10.339][10.339:396](),[10.68][10.98:103](),[10.396][10.98:103](),[10.98][10.98:103](),[10.103][10.1399:1444](),[10.609][10.1399:1444](),[10.8][10.1399:1444](),[10.1444][10.328:402](),[10.402][10.2412:2473](),[10.91][10.446:451](),[10.142][10.446:451](),[10.241][10.446:451](),[10.658][10.446:451](),[10.1023][10.446:451](),[10.1768][10.446:451](),[10.2473][10.446:451](),[10.446][10.446:451](),[10.451][10.64:110](),[10.110][10.403:477](),[10.477][10.2474:2536](),[10.290][10.130:135](),[10.667][10.130:135](),[10.2536][10.130:135](),[10.41][10.130:135](),[10.135][4.133:217]()
    local settings = {
    x=x, y=y, displayindex=displayindex,
    width=App.screen.width, height=App.screen.height,
    font_height=Editor_state.font_height,
    filename=filename,
    screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1}
    love.filesystem.write('config', json.encode(settings))
    end
    function App.mousepressed(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.mouse_pressed(Editor_state, x,y, mouse_button)
    end
    function App.mousereleased(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.mouse_released(Editor_state, x,y, mouse_button)
    end
    function App.focus(in_focus)
    if in_focus then
    Last_focus_time = App.getTime()
    [10.181]
    [4.217]
    --
    if chord == 'C-e' then
    -- carefully save settings
    if Current_app == 'run' then
    local source_settings = Settings.source
    Settings = run.settings()
    Settings.source = source_settings
    if run.quit then run.quit() end
    Current_app = 'source'
    elseif Current_app == 'source' then
    Settings.source = source.settings()
    if source.quit then source.quit() end
    Current_app = 'run'
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
    Settings.current_app = Current_app
    love.filesystem.write('config', json.encode(Settings))
    -- reboot
    load_file_from_source_or_save_directory('main.lua')
    App.undo_initialize()
    App.run_tests_and_initialize()
    return
    end
    if Current_app == 'run' then
    if run.keychord_pressed then run.keychord_pressed(chord, key) end
    elseif Current_app == 'source' then
    if source.keychord_pressed then source.keychord_pressed(chord, key) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
  • replacement in main.lua at line 188
    [10.1512][4.229:284]()
    -- ignore events for some time after window in focus
    [10.1512]
    [4.284]
    -- ignore events for some time after window in focus (mostly alt-tab)
  • replacement in main.lua at line 192
    [4.350][10.478:552](),[10.1512][10.478:552](),[10.552][10.2537:2578]()
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.textinput(Editor_state, t)
    [4.350]
    [10.3116]
    --
    if Current_app == 'run' then
    if run.textinput then run.textinput(t) end
    elseif Current_app == 'source' then
    if source.textinput then source.textinput(t) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 202
    [10.3121][10.794:836](),[10.836][4.351:406]()
    function App.keychord_pressed(chord, key)
    -- ignore events for some time after window in focus
    [10.3121]
    [4.406]
    function App.keyreleased(chord, key)
    -- ignore events for some time after window in focus (mostly alt-tab)
  • replacement in main.lua at line 207
    [4.472][10.553:627](),[10.836][10.553:627](),[10.627][10.2579:2636]()
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.keychord_pressed(Editor_state, chord, key)
    [4.472]
    [10.356]
    --
    if Current_app == 'run' then
    if run.key_released then run.key_released(chord, key) end
    elseif Current_app == 'source' then
    if source.key_released then source.key_released(chord, key) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • replacement in main.lua at line 217
    [10.361][10.1551:1591](),[10.1591][4.473:588]()
    function App.keyreleased(key, scancode)
    -- ignore events for some time after window in focus
    if App.getTime() < Last_focus_time + 0.01 then
    return
    [10.361]
    [4.588]
    function App.mousepressed(x,y, mouse_button)
    --? print('mouse press', x,y)
    if Current_app == 'run' then
    if run.mouse_pressed then run.mouse_pressed(x,y, mouse_button) end
    elseif Current_app == 'source' then
    if source.mouse_pressed then source.mouse_pressed(x,y, mouse_button) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
  • edit in main.lua at line 226
    [4.594][10.628:702](),[10.1591][10.628:702](),[10.702][10.2637:2693]()
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.key_released(Editor_state, key, scancode)
  • replacement in main.lua at line 228
    [9.102][9.102:235]()
    -- use this sparingly
    function to_text(s)
    if Text_cache[s] == nil then
    Text_cache[s] = App.newText(love.graphics.getFont(), s)
    [9.102]
    [9.235]
    function App.mousereleased(x,y, mouse_button)
    if Current_app == 'run' then
    if run.mouse_released then run.mouse_released(x,y, mouse_button) end
    elseif Current_app == 'source' then
    if source.mouse_released then source.mouse_released(x,y, mouse_button) end
    else
    assert(false, 'unknown app "'..Current_app..'"')
  • replacement in main.lua at line 236
    [9.241][9.241:264]()
    return Text_cache[s]
    [9.241]
    [9.264]
    end
    function love.quit()
    if Current_app == 'run' then
    local source_settings = Settings.source
    Settings = run.settings()
    Settings.source = source_settings
    else
    Settings.source = source.settings()
    end
    Settings.current_app = Current_app
    love.filesystem.write('config', json.encode(Settings))
    if Current_app == 'run' then
    if run.quit then run.quit() end
    elseif Current_app == 'source' then
    if source.quit then source.quit() end
    else
    assert(false, 'unknown app "'..Current_app..'"')
    end
  • file addition: log_browser.lua (----------)
    [10.2]
    -- environment for immutable logs
    -- optionally reads extensions for rendering some types from the source codebase that generated them
    --
    -- We won't care too much about long, wrapped lines. If they lines get too
    -- long to manage, you need a better, graphical rendering for them. Load
    -- functions to render them into the log_render namespace.
    function source.initialize_log_browser_side()
    Log_browser_state = edit.initialize_state(Margin_top, Editor_state.right + Margin_right + Margin_left, (Editor_state.right+Margin_right)*2, Editor_state.font_height, Editor_state.line_height)
    Log_browser_state.filename = 'log'
    load_from_disk(Log_browser_state) -- TODO: pay no attention to Fold
    log_browser.parse(Log_browser_state)
    Text.redraw_all(Log_browser_state)
    Log_browser_state.screen_top1 = {line=1, pos=1}
    Log_browser_state.cursor1 = {line=1, pos=nil}
    end
    Section_stack = {}
    Section_border_color = {r=0.7, g=0.7, b=0.7}
    Cursor_line_background_color = {r=0.7, g=0.7, b=0, a=0.1}
    Section_border_padding_horizontal = 30 -- TODO: adjust this based on font height (because we draw text vertically along the borders
    Section_border_padding_vertical = 15 -- TODO: adjust this based on font height
    log_browser = {}
    function log_browser.parse(State)
    for _,line in ipairs(State.lines) do
    if line.data ~= '' then
    line.filename, line.line_number, line.data = line.data:match('%[string "([^:]*)"%]:([^:]*):%s*(.*)')
    line.filename = guess_source(line.filename)
    line.line_number = tonumber(line.line_number)
    if line.data:sub(1,1) == '{' then
    local data = json.decode(line.data)
    if log_render[data.name] then
    line.data = data
    end
    line.section_stack = table.shallowcopy(Section_stack)
    elseif line.data:match('\u{250c}') then
    line.section_stack = table.shallowcopy(Section_stack) -- as it is at the beginning
    local section_name = line.data:match('\u{250c}%s*(.*)')
    table.insert(Section_stack, {name=section_name})
    line.section_begin = true
    line.section_name = section_name
    line.data = nil
    elseif line.data:match('\u{2518}') then
    local section_name = line.data:match('\u{2518}%s*(.*)')
    if array.find(Section_stack, function(x) return x.name == section_name end) then
    while table.remove(Section_stack).name ~= section_name do
    --
    end
    line.section_end = true
    line.section_name = section_name
    line.data = nil
    end
    line.section_stack = table.shallowcopy(Section_stack)
    else
    -- string
    line.section_stack = table.shallowcopy(Section_stack)
    end
    else
    line.section_stack = {}
    end
    end
    end
    function table.shallowcopy(x)
    return {unpack(x)}
    end
    function guess_source(filename)
    local possible_source = filename:gsub('%.lua$', '%.splua')
    if file_exists(possible_source) then
    return possible_source
    else
    return filename
    end
    end
    function log_browser.draw(State)
    assert(#State.lines == #State.line_cache)
    local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())
    local y = State.top
    for line_index = State.screen_top1.line,#State.lines do
    App.color(Text_color)
    local line = State.lines[line_index]
    if y + State.line_height > App.screen.height then break end
    local height = State.line_height
    if should_show(line) then
    local xleft = render_stack_left_margin(State, line_index, line, y)
    local xright = render_stack_right_margin(State, line_index, line, y)
    if line.section_name then
    App.color(Section_border_color)
    local section_text = to_text(line.section_name)
    if line.section_begin then
    local sectiony = y+Section_border_padding_vertical
    love.graphics.line(xleft,sectiony, xleft,y+State.line_height)
    love.graphics.line(xright,sectiony, xright,y+State.line_height)
    love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
    love.graphics.draw(section_text, xleft+50,y)
    love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
    else assert(line.section_end)
    local sectiony = y+State.line_height-Section_border_padding_vertical
    love.graphics.line(xleft,y, xleft,sectiony)
    love.graphics.line(xright,y, xright,sectiony)
    love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
    love.graphics.draw(section_text, xleft+50,y)
    love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
    end
    else
    if type(line.data) == 'string' then
    local old_left, old_right = State.left,State.right
    State.left,State.right = xleft,xright
    y = Text.draw(State, line_index, y, --[[startpos]] 1)
    State.left,State.right = old_left,old_right
    else
    height = log_render[line.data.name](line.data, xleft, y, xright-xleft)
    end
    end
    if App.mouse_x() > Log_browser_state.left and line_index == mouse_line_index then
    App.color(Cursor_line_background_color)
    love.graphics.rectangle('fill', xleft,y, xright-xleft, height)
    end
    y = y + height
    end
    end
    end
    function render_stack_left_margin(State, line_index, line, y)
    if line.section_stack == nil then
    -- assertion message
    for k,v in pairs(line) do
    print(k)
    end
    end
    App.color(Section_border_color)
    for i=1,#line.section_stack do
    local x = State.left + (i-1)*Section_border_padding_horizontal
    love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
    if y < 30 then
    love.graphics.print(line.section_stack[i].name, x+State.font_height+5, y+5, --[[vertically]] math.pi/2)
    end
    if y > App.screen.height-log_browser.height(State, line_index) then
    love.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
    end
    end
    return log_browser.left_margin(State, line)
    end
    function render_stack_right_margin(State, line_index, line, y)
    App.color(Section_border_color)
    for i=1,#line.section_stack do
    local x = State.right - (i-1)*Section_border_padding_horizontal
    love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
    if y < 30 then
    love.graphics.print(line.section_stack[i].name, x, y+5, --[[vertically]] math.pi/2)
    end
    if y > App.screen.height-log_browser.height(State, line_index) then
    love.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
    end
    end
    return log_browser.right_margin(State, line)
    end
    function should_show(line)
    -- Show a line if every single section it's in is expanded.
    for i=1,#line.section_stack do
    local section = line.section_stack[i]
    if not section.expanded then
    return false
    end
    end
    return true
    end
    function log_browser.left_margin(State, line)
    return State.left + #line.section_stack*Section_border_padding_horizontal
    end
    function log_browser.right_margin(State, line)
    return State.right - #line.section_stack*Section_border_padding_horizontal
    end
    function log_browser.update(State, dt)
    end
    function log_browser.quit(State)
    end
    function log_browser.mouse_pressed(State, x,y, mouse_button)
    local line_index = log_browser.line_index(State, x,y)
    if line_index == nil then
    -- below lower margin
    return
    end
    -- leave some space to click without focusing
    local line = State.lines[line_index]
    local xleft = log_browser.left_margin(State, line)
    local xright = log_browser.right_margin(State, line)
    if x < xleft or x > xright then
    return
    end
    -- if it's a section begin/end and the section is collapsed, expand it
    -- TODO: how to collapse?
    if line.section_begin or line.section_end then
    -- HACK: get section reference from next/previous line
    local new_section
    if line.section_begin then
    if line_index < #State.lines then
    local next_section_stack = State.lines[line_index+1].section_stack
    if next_section_stack then
    new_section = next_section_stack[#next_section_stack]
    end
    end
    elseif line.section_end then
    if line_index > 1 then
    local previous_section_stack = State.lines[line_index-1].section_stack
    if previous_section_stack then
    new_section = previous_section_stack[#previous_section_stack]
    end
    end
    end
    if new_section and new_section.expanded == nil then
    new_section.expanded = true
    return
    end
    end
    -- open appropriate file in source side
    if line.filename ~= Editor_state.filename then
    source.switch_to_file(line.filename)
    end
    -- set cursor
    Editor_state.cursor1 = {line=line.line_number, pos=1, posB=nil}
    -- make sure it's visible
    -- TODO: handle extremely long lines
    Editor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)
    -- show cursor
    Focus = 'edit'
    -- expand B side
    Editor_state.expanded = true
    end
    function log_browser.line_index(State, mx,my)
    -- duplicate some logic from log_browser.draw
    local y = State.top
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    if should_show(line) then
    y = y + log_browser.height(State, line_index)
    if my < y then
    return line_index
    end
    if y > App.screen.height then break end
    end
    end
    end
    function log_browser.mouse_released(State, x,y, mouse_button)
    end
    function log_browser.textinput(State, t)
    end
    function log_browser.keychord_pressed(State, chord, key)
    -- move
    if chord == 'up' then
    while State.screen_top1.line > 1 do
    State.screen_top1.line = State.screen_top1.line-1
    if should_show(State.lines[State.screen_top1.line]) then
    break
    end
    end
    elseif chord == 'down' then
    while State.screen_top1.line < #State.lines do
    State.screen_top1.line = State.screen_top1.line+1
    if should_show(State.lines[State.screen_top1.line]) then
    break
    end
    end
    elseif chord == 'pageup' then
    local y = 0
    while State.screen_top1.line > 1 and y < App.screen.height - 100 do
    State.screen_top1.line = State.screen_top1.line - 1
    if should_show(State.lines[State.screen_top1.line]) then
    y = y + log_browser.height(State, State.screen_top1.line)
    end
    end
    elseif chord == 'pagedown' then
    local y = 0
    while State.screen_top1.line < #State.lines and y < App.screen.height - 100 do
    if should_show(State.lines[State.screen_top1.line]) then
    y = y + log_browser.height(State, State.screen_top1.line)
    end
    State.screen_top1.line = State.screen_top1.line + 1
    end
    end
    end
    function log_browser.height(State, line_index)
    local line = State.lines[line_index]
    if line.data == nil then
    -- section header
    return State.line_height
    elseif type(line.data) == 'string' then
    return State.line_height
    else
    if line.height == nil then
    --? print('nil line height! rendering off screen to calculate')
    line.height = log_render[line.data.name](line.data, State.left, App.screen.height, State.right-State.left)
    end
    return line.height
    end
    end
    function log_browser.keyreleased(State, key, scancode)
    end
  • file addition: log.lua (----------)
    [10.2]
    function log(stack_frame_index, obj)
    local info = debug.getinfo(stack_frame_index, 'Sl')
    local msg
    if type(obj) == 'string' then
    msg = obj
    else
    msg = json.encode(obj)
    end
    love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n')
    end
    -- for section delimiters we'll use specific Unicode box characters
    function log_start(name, stack_frame_index)
    if stack_frame_index == nil then
    stack_frame_index = 3
    end
    log(stack_frame_index, '\u{250c} ' .. name)
    end
    function log_end(name, stack_frame_index)
    if stack_frame_index == nil then
    stack_frame_index = 3
    end
    log(stack_frame_index, '\u{2518} ' .. name)
    end
    function log_new(name, stack_frame_index)
    if stack_frame_index == nil then
    stack_frame_index = 4
    end
    log_end(name, stack_frame_index)
    log_start(name, stack_frame_index)
    end
    -- vim:noexpandtab
  • replacement in keychord.lua at line 59
    [10.228][10.228:293]()
    for i,x in ipairs(arr) do
    if x == elem then
    return i
    [10.228]
    [10.293]
    if type(elem) == 'function' then
    for i,x in ipairs(arr) do
    if elem(x) then
    return i
    end
  • edit in keychord.lua at line 65
    [10.301]
    [10.301]
    else
    for i,x in ipairs(arr) do
    if x == elem then
    return i
    end
    end
  • edit in edit.lua at line 10
    [10.22248][10.400:401](),[10.998][10.414:436](),[10.22249][10.414:436](),[10.413][10.414:436]()
    utf8 = require 'utf8'
  • edit in edit.lua at line 11
    [10.437][10.437:452](),[10.469][10.469:484](),[10.548][10.548:549]()
    require 'file'
    require 'text'
  • file addition: commands.lua (----------)
    [10.2]
    Menu_background_color = {r=0.6, g=0.8, b=0.6}
    Menu_border_color = {r=0.6, g=0.7, b=0.6}
    Menu_command_color = {r=0.2, g=0.2, b=0.2}
    Menu_highlight_color = {r=0.5, g=0.7, b=0.3}
    function source.draw_menu_bar()
    if App.run_tests then return end -- disable in tests
    App.color(Menu_background_color)
    love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)
    App.color(Menu_border_color)
    love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)
    App.color(Menu_command_color)
    Menu_cursor = 5
    if Show_file_navigator then
    source.draw_file_navigator()
    return
    end
    add_hotkey_to_menu('ctrl+e: run')
    if Focus == 'edit' then
    add_hotkey_to_menu('ctrl+g: switch file')
    if Show_log_browser_side then
    add_hotkey_to_menu('ctrl+l: hide log browser')
    else
    add_hotkey_to_menu('ctrl+l: show log browser')
    end
    if Editor_state.expanded then
    add_hotkey_to_menu('ctrl+b: collapse debug prints')
    else
    add_hotkey_to_menu('ctrl+b: expand debug prints')
    end
    add_hotkey_to_menu('ctrl+d: 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
  • file addition: colorize.lua (----------)
    [10.2]
    -- State transitions while colorizing a single line.
    -- Just for comments and strings.
    -- Limitation: each fragment gets a uniform color so we can only change color
    -- at word boundaries.
    Next_state = {
    normal={
    {prefix='--', target='comment'},
    {prefix='"', target='dstring'},
    {prefix="'", target='sstring'},
    },
    dstring={
    {suffix='"', target='normal'},
    },
    sstring={
    {suffix="'", target='normal'},
    },
    -- comments are a sink
    }
    Comments_color = {r=0, g=0, b=1}
    String_color = {r=0, g=0.5, b=0.5}
    Divider_color = {r=0.7, g=0.7, b=0.7}
    Colors = {
    normal=Text_color,
    comment=Comments_color,
    sstring=String_color,
    dstring=String_color
    }
    Current_state = 'normal'
    function initialize_color()
    --? print('new line')
    Current_state = 'normal'
    end
    function select_color(frag)
    --? print('before', '^'..frag..'$', Current_state)
    switch_color_based_on_prefix(frag)
    --? print('using color', Current_state, Colors[Current_state])
    App.color(Colors[Current_state])
    switch_color_based_on_suffix(frag)
    --? print('state after suffix', Current_state)
    end
    function switch_color_based_on_prefix(frag)
    if Next_state[Current_state] == nil then
    return
    end
    frag = rtrim(frag)
    for _,edge in pairs(Next_state[Current_state]) do
    if edge.prefix and find(frag, edge.prefix, nil, --[[plain]] true) == 1 then
    Current_state = edge.target
    break
    end
    end
    end
    function switch_color_based_on_suffix(frag)
    if Next_state[Current_state] == nil then
    return
    end
    frag = rtrim(frag)
    for _,edge in pairs(Next_state[Current_state]) do
    if edge.suffix and rfind(frag, edge.suffix, nil, --[[plain]] true) == #frag then
    Current_state = edge.target
    break
    end
    end
    end
    function trim(s)
    return s:gsub('^%s+', ''):gsub('%s+$', '')
    end
    function ltrim(s)
    return s:gsub('^%s+', '')
    end
    function rtrim(s)
    return s:gsub('%s+$', '')
    end
  • replacement in app.lua at line 1
    [10.2][10.1740:1769]()
    -- main entrypoint for LÖVE
    [10.2]
    [10.1769]
    -- love.run: main entrypoint function for LÖVE
  • edit in app.lua at line 14
    [10.54]
    [10.2266]
    App.snapshot_love()
  • replacement in app.lua at line 16
    [10.2302][10.2302:2321]()
    App.run_tests()
    [10.2302]
    [10.258]
    App.run_tests_and_initialize()
  • edit in app.lua at line 18
    [10.276][10.2321:2343](),[10.2321][10.2321:2343](),[10.2343][5.19:102]()
    App.disable_tests()
    App.initialize_globals()
    App.initialize(love.arg.parseGameArguments(arg), arg)
  • edit in app.lua at line 123
    [10.5849]
    [10.5849]
    -- save/restore various framework globals we care about -- only on very first load
    function App.snapshot_love()
    if Love_snapshot then return end
    Love_snapshot = {}
    -- save the entire initial font; it doesn't seem reliably recreated using newFont
    Love_snapshot.initial_font = love.graphics.getFont()
    end
    function App.undo_initialize()
    love.graphics.setFont(Love_snapshot.initial_font)
    end
    function App.run_tests_and_initialize()
    App.load()
    App.run_tests()
    App.disable_tests()
    App.initialize_globals()
    App.initialize(love.arg.parseGameArguments(arg), arg)
    end
  • edit in README.md at line 33
    [2.91]
    [166.18]
    * `ctrl+e` to modify the sources
  • edit in README.md at line 63
    [167.1120]
    [167.1120]
    * There are some temporary limitations when editing sources:
    - no line drawings
    - no selecting text
  • edit in README.md at line 85
    [8.163]
    [8.163]
    * https://codeberg.org/akkartik/pong.love -- a fairly minimal example app that
    can edit and debug its own source code.
  • edit in Manual_tests.md at line 5
    [10.1991]
    [10.210]
    Startup:
    - terminal log shows unit tests running
  • replacement in Manual_tests.md at line 9
    [10.211][10.23:168]()
    * Initializing settings:
    - from previous session
    - Filename as absolute path
    - Filename as relative path
    - from defaults
    [10.211]
    [10.1460]
    Initializing settings:
    - delete app settings, start; window opens running the text editor
    - quit while running the text editor, restart; window opens running the text editor in same position+dimensions
    - quit while editing source (color; no selection), restart; window opens editing source in same position+dimensions
    - start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions
    - start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions
    - no log file; switching to source works
    Code loading:
    * run love with directory; text editor runs
    * run love with zip file; text editor runs
  • edit in Manual_tests.md at line 26
    [10.169]
    [10.169]
    * start out running the text editor, press ctrl+e to edit source, make a change to the source, press ctrl+e twice to return to the source editor; the change should be preserved.