Merge text.love

[?]
Dec 24, 2022, 4:11 AM
C3GUE45IQNZNGKQVSINNGVVFEBWVWDT6Z4WVOEV5SQLGFUNPZXUQC

Dependencies

  • [2] 6BGU2SBJ Merge text.love
  • [3] THRPA4VV Merge text.love
  • [4] ORKN6EOB Merge lines.love
  • [5] 3QNOKBFM beginnings of a test harness
  • [6] 5UKUADTW distinguish consistently between mouse buttons and other buttons
  • [7] XNFTJHC4 split keyboard handling between Text and Drawing
  • [8] VXORMHME delete experimental REPL
  • [9] HYEAFRZ2 split mouse_pressed events between Text and Drawing
  • [10] AJB4LFRB try to maintain a reasonable line width
  • [11] LF7BWEG4 group all editor globals
  • [12] VHUNJHXB Merge lines.love
  • [13] 2Y7YH7UP infrastructure for caching LÖVE text objects
  • [14] KRDNIVZO Merge text.love
  • [15] QXVD2RIF add state arg to Drawing.mouse_released
  • [16] R3XGABER chunk up some long lines
  • [17] ATQO62TF Merge lines.love
  • [18] VO2ZVTWK Merge lines.love
  • [19] UYCVUHI5 Merge lines.love
  • [20] XRP727K3 Merge lines.love
  • [21] RXNR3U5E Merge text.love
  • [22] AYX33NBC Merge lines.love
  • [23] ETXNVRPT Merge lines.love
  • [24] UHB4GARJ left/right margin -> left/right coordinates
  • [25] 73OCE2MC after much struggle, a brute-force undo
  • [26] KTZQ57HV replace globals with args in a few functions
  • [27] P5QNVXSN drop final mention of state global beyond main.lua
  • [28] XX7G2FFJ intermingle freehand line drawings with text
  • [29] BULPIBEG beginnings of a module for the text editor
  • [30] DCO5BQWV Merge lines.love
  • [31] TO6Y2G3U more decoupling editor tests from App
  • [32] AT3LVCMP Merge text.love
  • [33] WPW3AVFS more precise shape selection
  • [34] TLOAPLBJ add a license
  • [35] FS2ITYYH record a known issue
  • [36] NYQ7HD4D move
  • [37] U2TKUOID bugfix: undo drawing creation
  • [38] 2CTN2IEF Merge lines.love
  • [39] AKZWDWIA Merge lines.love
  • [40] QS3YLNKZ Merge lines.love
  • [41] CE4LZV4T drop last couple of manual tests
  • [42] 3PSFWAIL Merge lines.love
  • [43] SCOXD4EO Merge lines.love
  • [44] RT6EV6OP delegate update events to drawings
  • [45] C45WCXJ2 keep drawings within the line width slider as well
  • [46] AVTNUQYR basic test-enabled framework
  • [47] D4B52CQ2 Merge lines.love
  • [48] VBU5YHLR Merge lines.love
  • [49] 36Z442IV back to commit 8123959e52f without code editing
  • [50] JIK7ZRYI bugfix: imprecision in drawing
  • [51] KYNGDE2C consistent names in a few more places
  • [52] JOPVPUSA editing source code from within the app
  • [53] 6DE7RBZ6 move mouse_released events to Drawing
  • [54] OGUV4HSA remove some memory leaks from rendered fragments
  • [55] TFM6F5OD Merge lines.love
  • [56] 4YDBYBA4 clean up memory leak experiments
  • [57] TVCPXAAU rename
  • [58] RSZD5A7G forgot to add json.lua
  • [59] BLWAYPKV extract a module
  • [60] VHQCNMAR several more modules
  • [61] D2GCFTTT clean up repl functionality
  • [62] BJ2C6F2B ignore 'name' mode in a few places
  • [63] L6XA5EY2 test: moving a point
  • [64] 6LJZN727 handle chords
  • [65] M6TH7VSZ rip out notion of Line_width
  • [66] 2L5MEZV3 experiment: new edit namespace
  • [67] LYN3L74W correct commit f3abc2cbf2
  • [68] R3JZDBI2 drop heavyweight near check on file load/store
  • [69] TXDMRA5J bugfix: alt-tab shouldn't emit keypress events
  • [70] MD3W5IRA new fork: rip out drawing support
  • [71] 5DOTWNVM right margin
  • [72] 3OTESDW6 move drawing.starty into line cache
  • [73] 32V6ZHQB Merge lines.love
  • [74] GUOQRUL7 Merge lines.love
  • [75] KKMFQDR4 editing source code from within the app
  • [76] KMSL74GA support selections in the source editor
  • [77] SHUFMMIV Merge text.love
  • [78] O3GEN2SD Merge text.love
  • [79] K2X6G75Z start writing some tests for drawings
  • [80] QFC3WRDZ chunking by simple local variable
  • [81] OTIBCAUJ love2d scaffold
  • [82] VZJHGWSP Merge lines.love
  • [83] LXTTOB33 extract a couple of files
  • [84] HRWN5V6J Devine's suggestion to try to live with just freehand
  • [85] JJDUDMVX Merge lines.love
  • [86] UZVWYRTY missing temporary modes in a couple more places
  • [87] S7ZZA3YE ugh, handle absolute as well as relative paths
  • [88] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing
  • [89] 66X36NZN a little more prose describing manual_tests
  • [90] DRFE3B3Z mouse buttons are integers, not strings
  • [91] T4FRZSYL delete an ancient, unused file
  • [92] DLQAEAC7 add state arg to Drawing.mouse_pressed
  • [93] 23MA4T3G add state arg to Drawing.keychord_pressed
  • [94] APX2PY6G stop tracking wallclock time
  • [95] PX7DDEMO autosave slightly less aggressively
  • [96] R5QXEHUI somebody stop me
  • [97] JCSLDGAH beginnings of support for multiple shapes
  • [98] NMRUNROT Merge lines.love
  • [99] LUNH47XX make text and drawings the same width
  • [100] 2CK5QI7W make love event names consistent
  • [101] VP5KC4XZ Merge lines.love
  • [102] 3HVBAZPA add state arg to a few functions

Change contents

  • file deletion: drawing.lua (----------)drawing.lua (----------)
    [5.2][5.1542:1577](),[5.1577][5.98:98](),[5.2][5.1542:1577]()
    function Drawing.keychord_press(State, chord)
    if chord == 'C-p' and not App.mouse_down(1) then
    State.current_drawing_mode = 'freehand'
    elseif App.mouse_down(1) and chord == 'l' then
    State.current_drawing_mode = 'line'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.p1 = drawing.pending.vertices[1]
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.p1 = drawing.pending.center
    end
    drawing.pending.mode = 'line'
    elseif chord == 'C-l' and not App.mouse_down(1) then
    State.current_drawing_mode = 'line'
    elseif App.mouse_down(1) and chord == 'm' then
    State.current_drawing_mode = 'manhattan'
    local drawing = Drawing.select_drawing_at_mouse(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'line' then
    -- do nothing
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.p1 = drawing.pending.vertices[1]
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.p1 = drawing.pending.center
    end
    drawing.pending.mode = 'manhattan'
    elseif chord == 'C-m' and not App.mouse_down(1) then
    State.current_drawing_mode = 'manhattan'
    elseif chord == 'C-g' and not App.mouse_down(1) then
    State.current_drawing_mode = 'polygon'
    elseif App.mouse_down(1) and chord == 'g' then
    State.current_drawing_mode = 'polygon'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    -- reuse existing vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'polygon'
    elseif chord == 'C-r' and not App.mouse_down(1) then
    State.current_drawing_mode = 'rectangle'
    elseif App.mouse_down(1) and chord == 'r' then
    State.current_drawing_mode = 'rectangle'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then
    -- reuse existing (1-2) vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'rectangle'
    elseif chord == 'C-s' and not App.mouse_down(1) then
    State.current_drawing_mode = 'square'
    elseif App.mouse_down(1) and chord == 's' then
    State.current_drawing_mode = 'square'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    if drawing.pending.vertices == nil then
    drawing.pending.vertices = {drawing.pending.p1}
    end
    elseif drawing.pending.mode == 'polygon' then
    while #drawing.pending.vertices > 2 do
    table.remove(drawing.pending.vertices)
    end
    elseif drawing.pending.mode == 'rectangle' then
    -- reuse existing (1-2) vertices
    elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
    drawing.pending.vertices = {drawing.pending.center}
    end
    drawing.pending.mode = 'square'
    elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    table.insert(drawing.pending.vertices, j)
    elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    while #drawing.pending.vertices >= 2 do
    table.remove(drawing.pending.vertices)
    end
    table.insert(drawing.pending.vertices, j)
    elseif chord == 'C-o' and not App.mouse_down(1) then
    State.current_drawing_mode = 'circle'
    elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' then
    local _,drawing,line_cache = Drawing.current_drawing(State)
    drawing.pending.mode = 'arc'
    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)
    local center = drawing.points[drawing.pending.center]
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my)
    elseif App.mouse_down(1) and chord == 'o' then
    State.current_drawing_mode = 'circle'
    local _,drawing = Drawing.current_drawing(State)
    if drawing.pending.mode == 'freehand' then
    drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
    elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
    drawing.pending.center = drawing.pending.p1
    elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
    drawing.pending.center = drawing.pending.vertices[1]
    end
    drawing.pending.mode = 'circle'
    elseif chord == 'C-u' and not App.mouse_down(1) then
    local drawing_index,drawing,line_cache,i,p = Drawing.select_point_at_mouse(State)
    if drawing then
    if State.previous_drawing_mode == nil then
    State.previous_drawing_mode = State.current_drawing_mode
    end
    State.current_drawing_mode = 'move'
    drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i}
    State.lines.current_drawing_index = drawing_index
    State.lines.current_drawing = drawing
    end
    elseif chord == 'C-n' and not App.mouse_down(1) then
    local drawing_index,drawing,line_cache,point_index,p = Drawing.select_point_at_mouse(State)
    if drawing then
    if State.previous_drawing_mode == nil then
    -- don't clobber
    State.previous_drawing_mode = State.current_drawing_mode
    end
    State.current_drawing_mode = 'name'
    p.name = ''
    drawing.pending = {mode=State.current_drawing_mode, target_point=point_index}
    State.lines.current_drawing_index = drawing_index
    State.lines.current_drawing = drawing
    end
    elseif chord == 'C-d' and not App.mouse_down(1) then
    local _,drawing,_,i,p = Drawing.select_point_at_mouse(State)
    if drawing then
    for _,shape in ipairs(drawing.shapes) do
    if Drawing.contains_point(shape, i) then
    if shape.mode == 'polygon' then
    local idx = table.find(shape.vertices, i)
    assert(idx)
    table.remove(shape.vertices, idx)
    if #shape.vertices < 3 then
    shape.mode = 'deleted'
    end
    else
    shape.mode = 'deleted'
    end
    end
    end
    drawing.points[i].deleted = true
    end
    local drawing,_,_,shape = Drawing.select_shape_at_mouse(State)
    if drawing then
    shape.mode = 'deleted'
    end
    elseif chord == 'C-h' and not App.mouse_down(1) then
    local drawing = Drawing.select_drawing_at_mouse(State)
    if drawing then
    drawing.show_help = true
    end
    elseif chord == 'escape' and App.mouse_down(1) then
    local _,drawing = Drawing.current_drawing(State)
    drawing.pending = {}
    end
    end
    function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)
    if firstx == secondx then
    return x,secondy, x,firsty
    end
    if firsty == secondy then
    return secondx,y, firstx,y
    end
    local first_slope = (secondy-firsty)/(secondx-firstx)
    -- slope of second edge:
    -- -1/first_slope
    -- equation of line containing the second edge:
    -- y-secondy = -1/first_slope*(x-secondx)
    -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
    -- now we want to find the point on this line that's closest to the mouse pointer.
    -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
    local a = 1/first_slope
    local c = -secondy - secondx/first_slope
    local thirdx = round(((x-a*y) - a*c) / (a*a + 1))
    local thirdy = round((a*(-x + a*y) - c) / (a*a + 1))
    -- slope of third edge = first_slope
    -- equation of line containing third edge:
    -- y - thirdy = first_slope*(x-thirdx)
    -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
    -- now we want to find the point on this line that's closest to the first point
    local a = -first_slope
    local c = -thirdy + thirdx*first_slope
    local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1))
    local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1))
    return thirdx,thirdy, fourthx,fourthy
    end
    function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y)
    -- use x,y only to decide which side of the first edge to complete the square on
    local deltax = secondx-firstx
    local deltay = secondy-firsty
    local thirdx = secondx+deltay
    local thirdy = secondy-deltax
    if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then
    deltax = -deltax
    deltay = -deltay
    thirdx = secondx+deltay
    thirdy = secondy-deltax
    end
    local fourthx = firstx+deltay
    local fourthy = firsty-deltax
    return thirdx,thirdy, fourthx,fourthy
    end
    function Drawing.current_drawing(State)
    local x, y = App.mouse_x(), App.mouse_y()
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    return drawing_index,drawing,line_cache
    end
    end
    end
    return nil
    end
    function Drawing.select_shape_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    for i,shape in ipairs(drawing.shapes) do
    assert(shape)
    if geom.on_shape(mx,my, drawing, shape) then
    return drawing,line_cache,i,shape
    end
    end
    end
    end
    end
    end
    function Drawing.select_point_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    for i,point in ipairs(drawing.points) do
    assert(point)
    if Drawing.near(point, mx,my, State.width) then
    return drawing_index,drawing,line_cache,i,point
    end
    end
    end
    end
    end
    end
    function Drawing.select_drawing_at_mouse(State)
    for drawing_index,drawing in ipairs(State.lines) do
    if drawing.mode == 'drawing' then
    local x, y = App.mouse_x(), App.mouse_y()
    local line_cache = State.line_cache[drawing_index]
    if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
    return drawing
    end
    end
    end
    end
    function Drawing.contains_point(shape, p)
    if shape.mode == 'freehand' then
    -- not supported
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
    return shape.p1 == p or shape.p2 == p
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
    return table.find(shape.vertices, p)
    elseif shape.mode == 'circle' then
    return shape.center == p
    elseif shape.mode == 'arc' then
    return shape.center == p
    -- ugh, how to support angles
    elseif shape.mode == 'deleted' then
    -- already done
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.smoothen(shape)
    assert(shape.mode == 'freehand')
    for _=1,7 do
    for i=2,#shape.points-1 do
    local a = shape.points[i-1]
    local b = shape.points[i]
    local c = shape.points[i+1]
    b.x = round((a.x + b.x + c.x)/3)
    b.y = round((a.y + b.y + c.y)/3)
    end
    end
    end
    function round(num)
    return math.floor(num+.5)
    end
    function Drawing.find_or_insert_point(points, x,y, width)
    -- check if UI would snap the two points together
    for i,point in ipairs(points) do
    if Drawing.near(point, x,y, width) then
    return i
    end
    end
    table.insert(points, {x=x, y=y})
    return #points
    end
    function Drawing.near(point, x,y, width)
    local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width)
    local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width)
    return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance
    end
    function Drawing.pixels(n, width) -- parts to pixels
    return math.floor(n*width/256)
    end
    function Drawing.coord(n, width) -- pixels to parts
    return math.floor(n*256/width)
    end
    function table.find(h, x)
    for k,v in pairs(h) do
    if v == x then
    return k
    end
    end
    end
    if chord == 'C-p' and not App.mouse_down(1) then
    function Drawing.mouse_release(State, x,y, mouse_button)
    if State.current_drawing_mode == 'move' then
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    if State.lines.current_drawing then
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    end
    elseif State.lines.current_drawing then
    local drawing = State.lines.current_drawing
    local line_cache = State.line_cache[State.lines.current_drawing_index]
    if drawing.pending then
    if drawing.pending.mode == nil then
    -- nothing pending
    elseif drawing.pending.mode == 'freehand' then
    -- the last point added during update is good enough
    Drawing.smoothen(drawing.pending)
    table.insert(drawing.shapes, drawing.pending)
    elseif drawing.pending.mode == 'line' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'manhattan' then
    local p1 = drawing.points[drawing.pending.p1]
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width)
    else
    drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width)
    end
    local p2 = drawing.points[drawing.pending.p2]
    App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'polygon' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'rectangle' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local first = drawing.points[drawing.pending.vertices[1]]
    local second = drawing.points[drawing.pending.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    else
    -- too few points; draw nothing
    end
    elseif drawing.pending.mode == 'square' then
    assert(#drawing.pending.vertices <= 2)
    if #drawing.pending.vertices == 2 then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local first = drawing.points[drawing.pending.vertices[1]]
    local second = drawing.points[drawing.pending.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
    table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
    table.insert(drawing.shapes, drawing.pending)
    end
    end
    elseif drawing.pending.mode == 'circle' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local center = drawing.points[drawing.pending.center]
    drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'arc' then
    local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
    if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
    local center = drawing.points[drawing.pending.center]
    drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle)
    table.insert(drawing.shapes, drawing.pending)
    end
    elseif drawing.pending.mode == 'name' then
    -- drop it
    else
    print(drawing.pending.mode)
    assert(false)
    end
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    end
    end
    end
    if State.current_drawing_mode == 'move' then
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    if State.lines.current_drawing then
    State.lines.current_drawing.pending = {}
    State.lines.current_drawing = nil
    -- all the action is in mouse_release
    elseif State.current_drawing_mode == 'name' then
    -- nothing
    else
    print(State.current_drawing_mode)
    assert(false)
    end
    end
    -- a couple of operations on drawings need to constantly check the state of the mouse
    function Drawing.update(State)
    if State.lines.current_drawing == nil then return end
    local drawing = State.lines.current_drawing
    local line_cache = State.line_cache[State.lines.current_drawing_index]
    assert(drawing.mode == 'drawing')
    local pmx, pmy = App.mouse_x(), App.mouse_y()
    local mx = Drawing.coord(pmx-State.left, State.width)
    local my = Drawing.coord(pmy-line_cache.starty, State.width)
    if App.mouse_down(1) then
    if Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) then
    if drawing.pending.mode == 'freehand' then
    table.insert(drawing.pending.points, {x=mx, y=my})
    elseif drawing.pending.mode == 'move' then
    drawing.pending.target_point.x = mx
    drawing.pending.target_point.y = my
    Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
    end
    end
    elseif State.current_drawing_mode == 'move' then
    if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then
    drawing.pending.target_point.x = mx
    drawing.pending.target_point.y = my
    Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
    end
    else
    -- do nothing
    end
    end
    function Drawing.relax_constraints(drawing, p)
    for _,shape in ipairs(drawing.shapes) do
    if shape.mode == 'manhattan' then
    if shape.p1 == p then
    shape.mode = 'line'
    elseif shape.p2 == p then
    shape.mode = 'line'
    end
    elseif shape.mode == 'rectangle' or shape.mode == 'square' then
    for _,v in ipairs(shape.vertices) do
    if v == p then
    shape.mode = 'polygon'
    end
    end
    end
    end
    end
    elseif State.current_drawing_mode == 'name' then
    -- after mouse_release
    function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    local cx = Drawing.coord(x-State.left, State.width)
    local cy = Drawing.coord(y-line_cache.starty, State.width)
    if State.current_drawing_mode == 'freehand' then
    drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}}
    elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, p1=j}
    elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, vertices={j}}
    elseif State.current_drawing_mode == 'circle' then
    local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
    drawing.pending = {mode=State.current_drawing_mode, center=j}
    elseif State.current_drawing_mode == 'move' then
    local drawing = State.lines[drawing_index]
    local line_cache = State.line_cache[drawing_index]
    pmx,pmy = px(mx), py(my)
    local shape = drawing.pending
    if shape.mode == nil then
    -- nothing pending
    elseif shape.mode == 'freehand' then
    local shape_copy = deepcopy(shape)
    Drawing.smoothen(shape_copy)
    Drawing.draw_shape(drawing, shape_copy, top, left,right)
    elseif shape.mode == 'line' then
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local p1 = drawing.points[shape.p1]
    love.graphics.line(px(p1.x),py(p1.y), pmx,pmy)
    elseif shape.mode == 'manhattan' then
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local p1 = drawing.points[shape.p1]
    if math.abs(mx-p1.x) > math.abs(my-p1.y) then
    love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y))
    else
    love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy)
    end
    elseif shape.mode == 'polygon' then
    -- don't close the loop on a pending polygon
    local prev = nil
    for _,point in ipairs(shape.vertices) do
    local curr = drawing.points[point]
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    end
    prev = curr
    end
    love.graphics.line(px(prev.x),py(prev.y), pmx,pmy)
    elseif shape.mode == 'rectangle' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    return
    end
    local second = drawing.points[shape.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
    love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))
    love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))
    love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))
    love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))
    elseif shape.mode == 'square' then
    local first = drawing.points[shape.vertices[1]]
    if #shape.vertices == 1 then
    love.graphics.line(px(first.x),py(first.y), pmx,pmy)
    return
    end
    local second = drawing.points[shape.vertices[2]]
    local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
    love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))
    love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))
    love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))
    love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))
    elseif shape.mode == 'circle' then
    local center = drawing.points[shape.center]
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    local cx,cy = px(center.x), py(center.y)
    love.graphics.circle('line', cx,cy, geom.dist(cx,cy, App.mouse_x(),App.mouse_y()))
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
    return
    end
    shape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle)
    local cx,cy = px(center.x), py(center.y)
    love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
    elseif shape.mode == 'move' then
    -- nothing pending; changes are immediately committed
    elseif shape.mode == 'name' then
    -- nothing pending; changes are immediately committed
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.in_drawing(drawing, line_cache, x,y, left,right)
    if line_cache.starty == nil then return false end -- outside current page
    local width = right-left
    return y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
    end
    pmx,pmy = px(mx), py(my)
    -- primitives for editing drawings
    Drawing = {}
    require 'drawing_tests'
    -- All drawings span 100% of some conceptual 'page width' and divide it up
    -- into 256 parts.
    function Drawing.draw(State, line_index, y)
    local line = State.lines[line_index]
    local line_cache = State.line_cache[line_index]
    line_cache.starty = y
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) then
    App.color(Icon_color)
    love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width))
    if icon[State.current_drawing_mode] then
    icon[State.current_drawing_mode](State.right-22, line_cache.starty+4)
    else
    icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4)
    end
    if App.mouse_down(1) and love.keyboard.isDown('h') then
    draw_help_with_mouse_pressed(State, line_index)
    return
    end
    end
    if line.show_help then
    draw_help_without_mouse_pressed(State, line_index)
    return
    end
    local mx = Drawing.coord(pmx-State.left, State.width)
    local my = Drawing.coord(pmy-line_cache.starty, State.width)
    for _,shape in ipairs(line.shapes) do
    assert(shape)
    if geom.on_shape(mx,my, line, shape) then
    App.color(Focus_stroke_color)
    else
    App.color(Stroke_color)
    end
    Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right)
    end
    local function px(x) return Drawing.pixels(x, State.width)+State.left end
    local function py(y) return Drawing.pixels(y, State.width)+line_cache.starty end
    for i,p in ipairs(line.points) do
    if p.deleted == nil then
    if Drawing.near(p, mx,my, State.width) then
    App.color(Focus_stroke_color)
    love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance)
    else
    App.color(Stroke_color)
    love.graphics.circle('fill', px(p.x),py(p.y), 2)
    end
    if p.name then
    -- TODO: clip
    local x,y = px(p.x)+5, py(p.y)+5
    love.graphics.print(p.name, x,y)
    if State.current_drawing_mode == 'name' and i == line.pending.target_point then
    -- create a faint red box for the name
    App.color(Current_name_background_color)
    local name_text
    -- TODO: avoid computing name width on every repaint
    if p.name == '' then
    name_text = State.em
    else
    name_text = App.newText(love.graphics.getFont(), p.name)
    end
    love.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height)
    end
    end
    end
    end
    App.color(Current_stroke_color)
    Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right)
    end
    function Drawing.draw_shape(drawing, shape, top, left,right)
    local width = right-left
    local function px(x) return Drawing.pixels(x, width)+left end
    local function py(y) return Drawing.pixels(y, width)+top end
    if shape.mode == 'freehand' then
    local prev = nil
    for _,point in ipairs(shape.points) do
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y))
    end
    prev = point
    end
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
    local p1 = drawing.points[shape.p1]
    local p2 = drawing.points[shape.p2]
    love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y))
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
    local prev = nil
    for _,point in ipairs(shape.vertices) do
    local curr = drawing.points[point]
    if prev then
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    end
    prev = curr
    end
    -- close the loop
    local curr = drawing.points[shape.vertices[1]]
    love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
    elseif shape.mode == 'circle' then
    -- TODO: clip
    local center = drawing.points[shape.center]
    love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width))
    elseif shape.mode == 'arc' then
    local center = drawing.points[shape.center]
    love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
    elseif shape.mode == 'deleted' then
    -- ignore
    else
    print(shape.mode)
    assert(false)
    end
    end
    function Drawing.draw_pending_shape(drawing, top, left,right)
    local width = right-left
    local pmx,pmy = App.mouse_x(), App.mouse_y()
    local function px(x) return Drawing.pixels(x, width)+left end
    local function py(y) return Drawing.pixels(y, width)+top end
    local mx = Drawing.coord(pmx-left, width)
    local my = Drawing.coord(pmy-top, width)
    -- recreate pixels from coords to precisely mimic how the drawing will look
  • file deletion: drawing_tests.lua (----------)drawing_tests.lua (----------)
    [5.2][5.1534:1575](),[5.2][5.1534:1575](),[5.1575][5.15:15]()
    Current_time = Current_time + 3.1
    edit.run_after_text_input(Editor_state, 'A')
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline')
    check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2')
    check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history')
    --? print('b', Editor_state.lines.current_drawing)
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    local drawing = Editor_state.lines[1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history')
    check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough
    -- wait until save
    edit.run_after_text_input(Editor_state, 'p') -- add point
    -- third point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
    check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices')
    -- hover on a point and delete
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_keychord(Editor_state, 'C-d')
    -- there's < 3 points left, so the whole polygon is deleted
    check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon')
    end
    function test_undo_name_point()
    io.write('\ntest_undo_name_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- draw a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y')
    check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name')
    check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1')
    --? print('a', Editor_state.lines.current_drawing)
    -- enter 'name' mode without moving the mouse
    edit.run_after_keychord(Editor_state, 'C-n')
    edit.run_after_text_input(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_text_input(Editor_state, 'p') -- add point
    -- fourth point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')
    check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices')
    -- hover on a point and delete
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_keychord(Editor_state, 'C-d')
    -- just the one point is deleted
    check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape')
    check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices')
    end
    function test_delete_point_from_polygon()
    io.write('\ntest_delete_point_from_polygon')
    -- create a drawing with two lines connected at a point
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_text_input(Editor_state, 'p') -- add point
    -- third point
    App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)
    edit.run_after_text_input(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    Current_time = Current_time + 3.1
    edit.run_after_text_input(Editor_state, 'A')
    check_eq(p2.name, 'A', 'F - test_name_point')
    -- still in 'name' mode
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2')
    -- exit 'name' mode
    edit.run_after_keychord(Editor_state, 'return')
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3')
    check_eq(p2.name, 'A', 'F - test_name_point')
    -- wait until save
    edit.run_after_text_input(Editor_state, 'p')
    -- release (decides which side of first edge to draw square on)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes')
    check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point added
    check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode')
    check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices')
    local p = drawing.points[drawing.shapes[1].vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_square/p1:x')
    check_eq(p.y, 36, 'F - test_draw_square/p1:y')
    local p = drawing.points[drawing.shapes[1].vertices[2]]
    check_eq(p.x, 65, 'F - test_draw_square/p2:x')
    check_eq(p.y, 66, 'F - test_draw_square/p2:y')
    local p = drawing.points[drawing.shapes[1].vertices[3]]
    check_eq(p.x, 35, 'F - test_draw_square/p3:x')
    check_eq(p.y, 96, 'F - test_draw_square/p3:y')
    local p = drawing.points[drawing.shapes[1].vertices[4]]
    check_eq(p.x, 5, 'F - test_draw_square/p4:x')
    check_eq(p.y, 66, 'F - test_draw_square/p4:y')
    end
    function test_name_point()
    io.write('\ntest_name_point')
    -- create a drawing with a line
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.filename = 'foo'
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    -- draw a line
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes')
    check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points')
    check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1')
    local p1 = drawing.points[drawing.shapes[1].p1]
    local p2 = drawing.points[drawing.shapes[1].p2]
    check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x')
    check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y')
    check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x')
    check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y')
    check_nil(p2.name, 'F - test_name_point/baseline/p2:name')
    -- enter 'name' mode without moving the mouse
    edit.run_after_keychord(Editor_state, 'C-n')
    check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1')
    edit.run_after_text_input(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66)
    edit.run_after_text_input(Editor_state, 's') -- square mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_text_input(Editor_state, 'p')
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point added
    local pending = drawing.pending
    check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode')
    check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices')
    local p = drawing.points[pending.vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x')
    check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y')
    local p = drawing.points[pending.vertices[2]]
    check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x')
    check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y')
    -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far
    end
    function test_draw_square()
    io.write('\ntest_draw_square')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_text_input(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    edit.run_after_text_input(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_text_input(Editor_state, 'p')
    -- release (decides 'thickness' of rectangle perpendicular to first edge)
    edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes')
    check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point added
    local shape = drawing.shapes[1]
    check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode')
    check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices')
    local p = drawing.points[shape.vertices[1]]
    check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x')
    check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y')
    local p = drawing.points[shape.vertices[2]]
    check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x')
    check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y')
    local p = drawing.points[shape.vertices[3]]
    check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x')
    check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y')
    local p = drawing.points[shape.vertices[4]]
    check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x')
    check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y')
    end
    function test_draw_rectangle_intermediate()
    io.write('\ntest_draw_rectangle_intermediate')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_text_input(Editor_state, 'p')
    -- override second point/first edge
    App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)
    edit.run_after_text_input(Editor_state, 'r') -- rectangle mode
    -- second point/first edge
    App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)
    edit.run_after_text_input(Editor_state, 'p') -- add point
    -- final point
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes')
    check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices')
    local shape = drawing.shapes[1]
    check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode')
    check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices')
    local p = drawing.points[shape.vertices[1]]
    check_eq(p.x, 5, 'F - test_draw_polygon/p1:x')
    check_eq(p.y, 6, 'F - test_draw_polygon/p1:y')
    local p = drawing.points[shape.vertices[2]]
    check_eq(p.x, 65, 'F - test_draw_polygon/p2:x')
    check_eq(p.y, 36, 'F - test_draw_polygon/p2:y')
    local p = drawing.points[shape.vertices[3]]
    check_eq(p.x, 35, 'F - test_draw_polygon/p3:x')
    check_eq(p.y, 26, 'F - test_draw_polygon/p3:y')
    end
    function test_draw_rectangle()
    io.write('\ntest_draw_rectangle')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    edit.run_after_text_input(Editor_state, 'g') -- polygon mode
    -- second point
    App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)
    edit.run_after_text_input(Editor_state, 'a') -- arc mode
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35+50, Editor_state.top+Drawing_padding_top+36+50, 1) -- 45°
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes')
    check_eq(#drawing.points, 1, 'F - test_draw_arc/#points')
    check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode')
    local arc = drawing.shapes[1]
    check_eq(arc.radius, 30, 'F - test_draw_arc/radius')
    local center = drawing.points[arc.center]
    check_eq(center.x, 35, 'F - test_draw_arc/center:x')
    check_eq(center.y, 36, 'F - test_draw_arc/center:y')
    check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle')
    check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle')
    end
    function test_draw_polygon()
    io.write('\ntest_draw_polygon')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    edit.draw(Editor_state)
    check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode')
    check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes')
    -- first point
    edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)
    edit.run_after_text_input(Editor_state, 'o')
    edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1)
    local drawing = Editor_state.lines[1]
    check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes')
    check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points')
    check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')
    check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius')
    local center = drawing.points[drawing.shapes[1].center]
    check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x')
    check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y')
    end
    function test_draw_arc()
    io.write('\ntest_draw_arc')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'circle'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes')
    -- draw an arc
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
    App.mouse_move(Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36)
    -- no change to text either because we didn't run the text_input event
    end
    function test_draw_circle_mid_stroke()
    io.write('\ntest_draw_circle_mid_stroke')
    -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
    App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'```lines', '```', ''}
    Text.redraw_all(Editor_state)
    Editor_state.current_drawing_mode = 'line'
    edit.draw(Editor_state)
    check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines')
    check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode')
    check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y')
    check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y')
    check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes')
    -- draw a circle
    App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing
    edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)
  • file deletion: source_text_tests.lua (----------)source_text_tests.lua (----------)
    [5.2][5.83739:83784](),[5.2][5.83739:83784](),[5.83784][5.3561:3561]()
    edit.run_after_text_input(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
    edit.run_after_text_input(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_text_input(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_text_input(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_text_input(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_text_input(Editor_state, 'x')
    check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline')
    check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection')
    -- undo
    edit.run_after_keychord(Editor_state, 'C-z')
    edit.run_after_keychord(Editor_state, 'C-z')
    -- selection is restored
    check_eq(Editor_state.selection1.line, 1, 'F - test_undo_restores_selection/line')
    check_eq(Editor_state.selection1.pos, 2, 'F - test_undo_restores_selection/pos')
    end
    edit.run_after_text_input(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')
    edit.run_after_text_input(Editor_state, 's')
    edit.run_after_text_input(Editor_state, 't')
    edit.run_after_text_input(Editor_state, 'u')
    check_eq(Editor_state.cursor1.pos, 28, '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_text_input(Editor_state, 'j')
    edit.run_after_text_input(Editor_state, 'k')
    edit.run_after_text_input(Editor_state, 'l')
    check_eq(Editor_state.screen_top1.line, 2, '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_text_input(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
    App.screen.check(y, 'mno', 'F - test_pagedown_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
    App.screen.check(y, 'kl', 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghij', 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')
    y = y + Editor_state.line_height
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/cursor:line')
    check_eq(Editor_state.cursor1.pos, 5, 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/cursor:pos')
    y = Editor_state.top
    check_eq(Editor_state.screen_top1.line, 3, 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/screen_top')
    check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_pagedown_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')
    App.screen.check(y, 'ghij', 'F - test_pagedown_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')
    App.screen.check(y, 'def', 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'abc', 'F - test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:1')
    y = y + Editor_state.line_height
    function test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up()
    io.write('\ntest_pagedown_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
    edit.run_after_text_input(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')
    edit.keychord_press(Editor_state, 'd', 'd')
    edit.text_input(Editor_state, 'D')
    edit.key_release(Editor_state, 'd')
    App.fake_key_release('lshift')
    -- selected text is deleted and replaced with the key
    check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection')
    check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
    end
    function test_copy_does_not_reset_selection()
    io.write('\ntest_copy_does_not_reset_selection')
    -- display a line of text with a selection
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.selection1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- copy selection
    edit.run_after_keychord(Editor_state, 'C-c')
    check_eq(App.clipboard, 'a', 'F - test_copy_does_not_reset_selection/clipboard')
    -- selection is reset since shift key is not pressed
    check(Editor_state.selection1.line, 'F - test_copy_does_not_reset_selection')
    end
    function test_cut()
    io.write('\ntest_cut')
    -- display a line of text with some part selected
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.selection1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- press a key
    edit.run_after_keychord(Editor_state, 'C-x')
    check_eq(App.clipboard, 'a', 'F - test_cut/clipboard')
    -- selected text is deleted
    check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')
    end
    function test_paste_replaces_selection()
    io.write('\ntest_paste_replaces_selection')
    -- display a line of text with a selection
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=2, pos=1}
    Editor_state.selection1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- set clipboard
    App.clipboard = 'xyz'
    -- paste selection
    edit.run_after_keychord(Editor_state, 'C-v')
    -- selection is reset since shift key is not pressed
    -- selection includes the newline, so it's also deleted
    check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection')
    end
    function test_deleting_selection_may_scroll()
    io.write('\ntest_deleting_selection_may_scroll')
    -- display lines 2/3/4
    App.screen.init{width=120, height=60}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=3, pos=2}
    Editor_state.screen_top1 = {line=2, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    local y = Editor_state.top
    App.screen.check(y, 'def', 'F - test_deleting_selection_may_scroll/baseline/screen:1')
    y = y + Editor_state.line_height
    App.screen.check(y, 'ghi', 'F - test_deleting_selection_may_scroll/baseline/screen:2')
    y = y + Editor_state.line_height
    App.screen.check(y, 'jkl', 'F - test_deleting_selection_may_scroll/baseline/screen:3')
    -- set up a selection starting above the currently displayed page
    Editor_state.selection1 = {line=1, pos=2}
    -- delete selection
    edit.run_after_keychord(Editor_state, 'backspace')
    -- page scrolls up
    check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll')
    check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')
    end
    edit.run_after_text_input(Editor_state, 'x')
    -- selected text is deleted and replaced with the key
    check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')
    end
    function test_edit_with_shift_key_deletes_selection()
    io.write('\ntest_edit_with_shift_key_deletes_selection')
    -- display a line of text with some part selected
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.selection1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- mimic precise keypresses for a capital letter
    App.fake_key_press('lshift')
    edit.key_release(Editor_state, 'lshift')
    -- selection persists even after shift is release
    check_eq(Editor_state.selection1.line, 1, 'F - test_select_text/selection:line')
    check_eq(Editor_state.selection1.pos, 1, 'F - test_select_text/selection:pos')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_select_text/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text/cursor:pos')
    end
    function test_cursor_movement_without_shift_resets_selection()
    io.write('\ntest_cursor_movement_without_shift_resets_selection')
    -- display a line of text with some part selected
    App.screen.init{width=75, height=80}
    Editor_state = edit.initialize_test_state()
    Editor_state.lines = load_array{'abc'}
    Text.redraw_all(Editor_state)
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.selection1 = {line=1, pos=2}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    edit.draw(Editor_state)
    -- press an arrow key without shift
    edit.run_after_keychord(Editor_state, 'right')
    -- no change to data, selection is reset
    check_nil(Editor_state.selection1.line, 'F - test_cursor_movement_without_shift_resets_selection')
    check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line_takes_margins_into_account/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_on_wrapping_line_takes_margins_into_account/cursor:pos')
    check_nil(Editor_state.selection1.line, 'F - test_click_on_wrapping_line_takes_margins_into_account/selection is empty to avoid perturbing future edits')
    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')
    function test_click_on_wrapping_line_takes_margins_into_account()
    io.write('\ntest_click_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_on_wrapping_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_on_wrapping_line/cursor:pos')
    check_nil(Editor_state.selection1.line, 'F - test_click_on_wrapping_line/selection is empty to avoid perturbing future edits')
    end
    function test_click_on_wrapping_line()
    io.write('\ntest_click_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_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_on_empty_line()
    io.write('\ntest_click_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_takes_margins_into_account/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_takes_margins_into_account/cursor:pos')
    check_nil(Editor_state.selection1.line, 'F - test_click_takes_margins_into_account/selection is empty to avoid perturbing future edits')
    end
    function test_click_takes_margins_into_account()
    io.write('\ntest_click_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_to_left_of_line/cursor:line')
    check_eq(Editor_state.cursor1.pos, 1, 'F - test_click_to_left_of_line/cursor:pos')
    check_nil(Editor_state.selection1.line, 'F - test_click_to_left_of_line/selection is empty to avoid perturbing future edits')
    end
    function test_click_to_left_of_line()
    io.write('\ntest_click_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
    Editor_state.selection1 = {}
    edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache
    edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
    check_eq(Editor_state.cursor1.line, 1, 'F - test_click_moves_cursor/cursor:line')
    check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_moves_cursor/cursor:pos')
    -- selection is empty to avoid perturbing future edits
    check_nil(Editor_state.selection1.line, 'F - test_click_moves_cursor/selection:line')
    check_nil(Editor_state.selection1.pos, 'F - test_click_moves_cursor/selection:pos')
    end
    Editor_state.cursor1 = {line=1, pos=1}
    Editor_state.screen_top1 = {line=1, pos=1}
    Editor_state.screen_bottom1 = {}
    Editor_state.lines = load_array{'abc', 'def', 'xyz'}
    Text.redraw_all(Editor_state)
    function test_click_moves_cursor()
    io.write('\ntest_click_moves_cursor')
    App.screen.init{width=50, height=60}
    Editor_state = edit.initialize_test_state()
    edit.run_after_text_input(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
  • file deletion: source_text.lua (----------)source_text.lua (----------)
    [5.2][5.147125:147164](),[5.2][5.147125:147164](),[5.147164][5.83786:83786]()
    -- Don't handle any keys here that would trigger text_input above.
    function Text.keychord_press(State, chord)
    --? print('chord', chord, State.selection1.line, State.selection1.pos)
    function Text.text_input(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
  • file deletion: source_edit.lua (----------)source_edit.lua (----------)
    [5.2][5.165788:165827](),[5.2][5.165788:165827](),[5.165827][5.152503:152503]()
    edit.mouse_release(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.mouse_press(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.mouse_release(State, x,y, mouse_button)
    App.screen.contents = {}
    edit.mouse_press(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.keychord_press(State, chord)
    edit.key_release(State, chord)
    App.screen.contents = {}
    -- not all keys are text_input
    function edit.run_after_keychord(State, chord)
    function edit.run_after_text_input(State, t)
    edit.keychord_press(State, t)
    edit.text_input(State, t)
    edit.key_release(State, t)
    App.screen.contents = {}
    -- all text_input events are also keypresses
    -- TODO: handle chords of multiple keys
    function edit.key_release(State, key, scancode)
    end
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    Text.keychord_press(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
    Drawing.keychord_press(State, chord)
    record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
    schedule_save(State)
    end
    elseif chord == 'escape' and not App.mouse_down(1) then
    for _,line in ipairs(State.lines) do
    if line.mode == 'drawing' then
    line.show_help = false
    end
    end
    elseif State.current_drawing_mode == 'name' then
    if chord == 'return' then
    State.current_drawing_mode = State.previous_drawing_mode
    State.previous_drawing_mode = nil
    else
    local before = snapshot(State, State.lines.current_drawing_index)
    local drawing = State.lines.current_drawing
    local p = drawing.points[drawing.pending.target_point]
    if chord == 'escape' then
    p.name = nil
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    elseif chord == 'backspace' then
    local len = utf8.len(p.name)
    local byte_offset = Text.offset(p.name, len-1)
    if len == 1 then byte_offset = 0 end
    p.name = string.sub(p.name, 1, byte_offset)
    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
    end
    end
    schedule_save(State)
    function edit.keychord_press(State, chord, key)
    if State.selection1.line and
    not State.lines.current_drawing and
    -- printable character created using shift key => delete selection
    -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
    (not App.shift_down() or utf8.len(key) == 1) and
    Text.text_input(State, t)
    end
    function edit.text_input(State, t)
    if State.search_term then
    State.search_term = State.search_term..t
    State.search_text = nil
    Text.search_next(State)
    Drawing.mouse_release(State, x,y, mouse_button)
    schedule_save(State)
    if Drawing.before then
    record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
    Drawing.before = nil
    end
    function edit.mouse_release(State, x,y, mouse_button)
    if State.search_term then return end
    --? print('release')
    if State.lines.current_drawing then
    Drawing.mouse_press(State, line_index, x,y, mouse_button)
    break
    end
    -- i.e. mouse_release should never look at shift state
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    function edit.mouse_press(State, x,y, mouse_button)
    if State.search_term then return end
  • file deletion: source.lua (----------)source.lua (----------)
    [5.2][5.177715:177749](),[5.2][5.177715:177749](),[5.177749][5.165829:165829]()
    return log_browser.keychord_press(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
    return edit.key_release(Editor_state, key, scancode)
    else
    function source.key_release(key, scancode)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    if Focus == 'edit' then
    return log_browser.keychord_press(Log_browser_state, chord, key)
    end
    end
    return edit.keychord_press(Editor_state, chord, key)
    else
    keychord_press_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
    function source.keychord_press(chord, key)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    --? print('source keychord')
    if Show_file_navigator then
    return log_browser.text_input(Log_browser_state, t)
    end
    end
    return edit.text_input(Editor_state, t)
    else
    text_input_on_file_navigator(t)
    return
    end
    function source.text_input(t)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return log_browser.mouse_release(Log_browser_state, x,y, mouse_button)
    end
    end
    return edit.mouse_release(Editor_state, x,y, mouse_button)
    else
    function source.mouse_release(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    if Focus == 'edit' then
    log_browser.mouse_press(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
    edit.mouse_press(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'
    edit.mouse_press(Editor_state, x,y, mouse_button)
    return
    end
    function source.mouse_press(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)
    -- a copy of source.file_drop 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
    function source.file_drop(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}
  • file deletion: run.lua (----------)run.lua (----------)
    [5.2][5.183867:183898](),[5.2][5.183867:183898](),[5.183898][5.178107:178107]()
    return edit.key_release(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
    function run.key_release(key, scancode)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.keychord_press(Editor_state, chord, key)
    end
    function run.keychord_press(chord, key)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.text_input(Editor_state, t)
    end
    function run.text_input(t)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    return edit.mouse_release(Editor_state, x,y, mouse_button)
    end
    function run.mouse_release(x,y, mouse_button)
    Cursor_time = 0 -- ensure cursor is visible immediately after it moves
    function run.file_drop(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()
  • file deletion: log_browser.lua (----------)log_browser.lua (----------)
    [5.2][5.202684:202723](),[5.2][5.202684:202723](),[5.202723][5.191243:191243]()
    function log_browser.key_release(State, key, scancode)
    end
    function log_browser.keychord_press(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.text_input(State, t)
    end
    function log_browser.mouse_release(State, x,y, mouse_button)
    end
    function log_browser.mouse_press(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
  • file deletion: commands.lua (----------)commands.lua (----------)
    [5.2][5.207182:207218](),[5.2][5.207182:207218](),[5.207218][5.203826:203826]()
    function text_input_on_file_navigator(t)
    File_navigation.filter = File_navigation.filter..t
    File_navigation.candidates = source.file_navigator_candidates()
    end
    function keychord_press_on_file_navigator(chord, key)
    log(2, 'file navigator: '..chord)
    log(2, {name='file_navigator_state', files=File_navigation.candidates, index=File_navigation.index})
  • edit in main.lua at line 59
    [5.187701][4.8062:8093](),[5.187701][4.8062:8093](),[4.8093][5.4429:4479](),[5.187732][5.4429:4479](),[5.4479][4.8094:8132](),[4.8132][5.4480:4536](),[5.187824][5.4480:4536]()
    if Current_app == 'run' then
    if run.file_drop then run.file_drop(file) end
    elseif Current_app == 'source' then
    if source.file_drop then source.file_drop(file) end
  • resolve order conflict in main.lua at line 59
    [5.3019]
    [5.187401]
  • edit in main.lua at line 188
    [5.7306][5.7306:7363](),[5.189628][5.4578:4644](),[5.189628][5.4578:4644](),[5.189736][5.4645:4717](),[5.189736][5.4645:4717]()
    return edit.keychord_pressed(Editor_state, chord, key)
    if run.keychord_press then run.keychord_press(chord, key) end
    if source.keychord_press then source.keychord_press(chord, key) end
  • resolve order conflict in main.lua at line 188
    [5.7306]
  • edit in main.lua at line 188
    [0.11]
    [5.223]
    return edit.keychord_press(Editor_state, chord, key)
  • edit in main.lua at line 197
    [5.7438][5.7438:7479](),[5.189982][5.4718:4767](),[5.189982][5.4718:4767](),[5.190067][5.4768:4823](),[5.190067][5.4768:4823]()
    return edit.textinput(Editor_state, t)
    if run.text_input then run.text_input(t) end
    if source.text_input then source.text_input(t) end
  • resolve order conflict in main.lua at line 197
    [5.7438]
  • edit in main.lua at line 197
    [0.69]
    [5.3116]
    return edit.text_input(Editor_state, t)
  • edit in main.lua at line 204
    [5.190333][5.4824:4884](),[5.190333][5.4824:4884](),[5.190433][5.4885:4951](),[5.190433][5.4885:4951]()
    if run.key_release then run.key_release(chord, key) end
    if source.key_release then source.key_release(chord, key) end
  • edit in main.lua at line 206
    [5.7595][5.7595:7651](),[5.190461][4.8324:8395](),[5.190461][4.8324:8395](),[5.190572][4.8396:8473](),[5.190572][4.8396:8473]()
    return edit.key_released(Editor_state, key, scancode)
    if run.mouse_release then run.mouse_release(x,y, mouse_button) end
    if source.mouse_release then source.mouse_release(x,y, mouse_button) end
  • resolve order conflict in main.lua at line 206
    [5.7595]
  • edit in main.lua at line 206
    [0.114]
    [5.190712]
    return edit.key_release(Editor_state, key, scancode)
  • edit in main.lua at line 216
    [5.268][5.473:473](),[5.190137][4.8176:8245](),[5.190137][4.8176:8245](),[5.190246][4.8247:8247](),[5.190246][4.8247:8247](),[4.8247][4.8248:8323](),[4.8247][4.8248:8323]()
    if run.mouse_pressed then run.mouse_press(x,y, mouse_button) end
    if source.mouse_pressed then source.mouse_press(x,y, mouse_button) end
  • resolve order conflict in main.lua at line 216
    [5.268]