about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--app.lua5
-rw-r--r--drawing.lua107
-rw-r--r--drawing_tests.lua22
-rw-r--r--edit.lua24
-rw-r--r--help.lua12
-rw-r--r--search.lua6
-rw-r--r--select.lua5
-rw-r--r--source.lua1
-rw-r--r--source_edit.lua30
-rw-r--r--source_select.lua5
-rw-r--r--source_text.lua175
-rw-r--r--source_text_tests.lua98
-rw-r--r--text.lua149
-rw-r--r--text_tests.lua95
14 files changed, 343 insertions, 391 deletions
diff --git a/app.lua b/app.lua
index 627a438..263518c 100644
--- a/app.lua
+++ b/app.lua
@@ -130,6 +130,8 @@ function App.run_tests()
     end
   end
   table.sort(sorted_names)
+--?   App.initialize_for_test() -- debug: run a single test at a time like these 2 lines
+--?   test_click_below_all_lines()
   for _,name in ipairs(sorted_names) do
     App.initialize_for_test()
 --?     print('=== '..name)
@@ -404,9 +406,10 @@ end
 -- prepend file/line/test
 function prepend_debug_info_to_test_failure(test_name, err)
   local err_without_line_number = err:gsub('^[^:]*:[^:]*: ', '')
-  local stack_trace = debug.traceback('', --[[stack frame]]5)
+  local stack_trace = debug.traceback('', --[[stack frame]]5)  -- most likely to be useful, but set to 0 for a complete stack trace
   local file_and_line_number = stack_trace:gsub('stack traceback:\n', ''):gsub(': .*', '')
   local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number
+  -- uncomment this line for a complete stack trace
 --?   local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number..'\t\t'..stack_trace:gsub('\n', '\n\t\t')
   table.insert(Test_errors, full_error)
 end
diff --git a/drawing.lua b/drawing.lua
index 246c7ae..92e3d5f 100644
--- a/drawing.lua
+++ b/drawing.lua
@@ -6,16 +6,15 @@ require 'drawing_tests'
 -- 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
+  local starty = Text.starty(State, line_index)
+  if pmx < State.right and pmy > starty and pmy < 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))
+    love.graphics.rectangle('line', State.left,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)
+      icon[State.current_drawing_mode](State.right-22, starty+4)
     else
-      icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4)
+      icon[State.previous_drawing_mode](State.right-22, starty+4)
     end
 
     if App.mouse_down(1) and love.keyboard.isDown('h') then
@@ -30,7 +29,7 @@ function Drawing.draw(State, line_index, y)
   end
 
   local mx = Drawing.coord(pmx-State.left, State.width)
-  local my = Drawing.coord(pmy-line_cache.starty, State.width)
+  local my = Drawing.coord(pmy-starty, State.width)
 
   for _,shape in ipairs(line.shapes) do
     if geom.on_shape(mx,my, line, shape) then
@@ -38,11 +37,11 @@ function Drawing.draw(State, line_index, y)
     else
       App.color(Stroke_color)
     end
-    Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right)
+    Drawing.draw_shape(line, shape, 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
+  local function py(y) return Drawing.pixels(y, State.width)+starty end
   for i,p in ipairs(line.points) do
     if p.deleted == nil then
       if Drawing.near(p, mx,my, State.width) then
@@ -71,7 +70,7 @@ function Drawing.draw(State, line_index, y)
     end
   end
   App.color(Current_stroke_color)
-  Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right)
+  Drawing.draw_pending_shape(line, starty, State.left,State.right)
 end
 
 function Drawing.draw_shape(drawing, shape, top, left,right)
@@ -209,17 +208,24 @@ function Drawing.draw_pending_shape(drawing, top, left,right)
   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
+function Drawing.in_current_drawing(State, x,y, left,right)
+  return Drawing.in_drawing(State, State.lines.current_drawing_index, x,y, left,right)
+end
+
+function Drawing.in_drawing(State, line_index, x,y, left,right)
+  assert(State.lines[line_index].mode == 'drawing')
+  local starty = Text.starty(State, line_index)
+  if starty == nil then return false end  -- outside current page
+  local drawing = State.lines[line_index]
   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
+  return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
 end
 
 function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)
   local drawing = State.lines[drawing_index]
-  local line_cache = State.line_cache[drawing_index]
+  local starty = Text.starty(State, drawing_index)
   local cx = Drawing.coord(x-State.left, State.width)
-  local cy = Drawing.coord(y-line_cache.starty, State.width)
+  local cy = Drawing.coord(y-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
@@ -244,8 +250,8 @@ end
 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]
-  if line_cache.starty == nil then
+  local starty = Text.starty(State, State.lines.current_drawing_index)
+  if starty == nil then
     -- some event cleared starty just this frame
     -- draw in this frame will soon set starty
     -- just skip this frame
@@ -254,9 +260,9 @@ function Drawing.update(State)
   assert(drawing.mode == 'drawing', 'Drawing.update: line is not a 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)
+  local my = Drawing.coord(pmy-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.in_current_drawing(State, 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
@@ -266,7 +272,7 @@ function Drawing.update(State)
       end
     end
   elseif State.current_drawing_mode == 'move' then
-    if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then
+    if Drawing.in_current_drawing(State, 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)
@@ -304,7 +310,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
     end
   elseif State.lines.current_drawing then
     local drawing = State.lines.current_drawing
-    local line_cache = State.line_cache[State.lines.current_drawing_index]
+    local starty = Text.starty(State, State.lines.current_drawing_index)
     if drawing.pending then
       if drawing.pending.mode == nil then
         -- nothing pending
@@ -313,14 +319,14 @@ function Drawing.mouse_release(State, x,y, mouse_button)
         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)
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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)
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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)
@@ -328,11 +334,11 @@ function Drawing.mouse_release(State, x,y, mouse_button)
             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))
+          App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), 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)
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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)
@@ -340,7 +346,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
       elseif drawing.pending.mode == 'rectangle' then
         assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
         if #drawing.pending.vertices == 2 then
-          local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
+          local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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]]
@@ -355,7 +361,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
       elseif drawing.pending.mode == 'square' then
         assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: square has too many pending vertices')
         if #drawing.pending.vertices == 2 then
-          local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
+          local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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]]
@@ -366,14 +372,14 @@ function Drawing.mouse_release(State, x,y, mouse_button)
           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)
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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)
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-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)
@@ -477,13 +483,15 @@ function Drawing.keychord_press(State, chord)
     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 drawing_index,drawing = Drawing.current_drawing(State)
+    local starty = Text.starty(State, drawing_index)
+    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-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 drawing_index,drawing = Drawing.current_drawing(State)
+    local starty = Text.starty(State, drawing_index)
+    local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-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)
@@ -492,9 +500,10 @@ function Drawing.keychord_press(State, chord)
   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)
+    local drawing_index,drawing = Drawing.current_drawing(State)
+    local starty = Text.starty(State, drawing_index)
     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 mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-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)
@@ -510,7 +519,7 @@ function Drawing.keychord_press(State, chord)
     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)
+    local drawing_index,drawing,_,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
@@ -521,7 +530,7 @@ function Drawing.keychord_press(State, chord)
       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)
+    local drawing_index,drawing,_,point_index,p = Drawing.select_point_at_mouse(State)
     if drawing then
       if State.previous_drawing_mode == nil then
         -- don't clobber
@@ -619,9 +628,8 @@ 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
+      if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
+        return drawing_index,drawing
       end
     end
   end
@@ -632,12 +640,12 @@ 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)
+      local starty = Text.starty(State, drawing_index)
+      if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
         for i,shape in ipairs(drawing.shapes) do
           if geom.on_shape(mx,my, drawing, shape) then
-            return drawing,line_cache,i,shape
+            return drawing,starty,i,shape
           end
         end
       end
@@ -649,12 +657,12 @@ 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)
+      local starty = Text.starty(State, drawing_index)
+      if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
+        local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
         for i,point in ipairs(drawing.points) do
           if Drawing.near(point, mx,my, State.width) then
-            return drawing_index,drawing,line_cache,i,point
+            return drawing_index,drawing,starty,i,point
           end
         end
       end
@@ -666,8 +674,7 @@ 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
+      if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
         return drawing
       end
     end
diff --git a/drawing_tests.lua b/drawing_tests.lua
index ede25f0..3484436 100644
--- a/drawing_tests.lua
+++ b/drawing_tests.lua
@@ -3,7 +3,7 @@
 -- of specific shapes. In particular, no tests of freehand drawings.
 
 function test_creating_drawing_saves()
-  App.screen.init{width=120, height=60}
+  App.screen.init{width=800, height=600}
   Editor_state = edit.initialize_test_state()
   Editor_state.filename = 'foo'
   Editor_state.lines = load_array{}
@@ -32,7 +32,7 @@ function test_draw_line()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- draw a line
@@ -77,7 +77,7 @@ function test_draw_horizontal_line()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- draw a line that is more horizontal than vertical
@@ -105,7 +105,7 @@ function test_draw_circle()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- draw a circle
@@ -134,7 +134,7 @@ function test_cancel_stroke()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- start drawing a line
@@ -172,7 +172,7 @@ function test_draw_circle_mid_stroke()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- draw a circle
@@ -200,7 +200,7 @@ function test_draw_arc()
   edit.draw(Editor_state)
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- draw an arc
@@ -231,7 +231,7 @@ function test_draw_polygon()
   check_eq(Editor_state.current_drawing_mode, 'line', 'baseline/drawing_mode')
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- first point
@@ -269,7 +269,7 @@ function test_draw_rectangle()
   check_eq(Editor_state.current_drawing_mode, 'line', 'baseline/drawing_mode')
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- first point
@@ -313,7 +313,7 @@ function test_draw_rectangle_intermediate()
   check_eq(Editor_state.current_drawing_mode, 'line', 'baseline/drawing_mode')
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- first point
@@ -349,7 +349,7 @@ function test_draw_square()
   check_eq(Editor_state.current_drawing_mode, 'line', 'baseline/drawing_mode')
   check_eq(#Editor_state.lines, 2, 'baseline/#lines')
   check_eq(Editor_state.lines[1].mode, 'drawing', 'baseline/mode')
-  check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'baseline/y')
+  check_eq(Text.starty(Editor_state, 1), Editor_state.top+Drawing_padding_top, 'baseline/y')
   check_eq(Editor_state.lines[1].h, 128, 'baseline/y')
   check_eq(#Editor_state.lines[1].shapes, 0, 'baseline/#shapes')
   -- first point
diff --git a/edit.lua b/edit.lua
index 79a2fc3..6a7d708 100644
--- a/edit.lua
+++ b/edit.lua
@@ -19,7 +19,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
 
     -- rendering wrapped text lines needs some additional short-lived data per line:
     --   startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen
-    --   starty, the y coord in pixels the line starts rendering from
     --   fragments: snippets of the line guaranteed to not straddle screen lines
     --   screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
     line_cache = {},
@@ -33,9 +32,10 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
     --
     -- Make sure these coordinates are never aliased, so that changing one causes
     -- action at a distance.
+    --
+    -- On lines that are drawings, pos will be nil.
     screen_top1 = {line=1, pos=1},  -- position of start of screen line at top of screen
-    cursor1 = {line=1, pos=1},  -- position of cursor
-    screen_bottom1 = {line=1, pos=1},  -- position of start of screen line at bottom of screen
+    cursor1 = {line=1, pos=1},  -- position of cursor; must be on a text line
 
     selection1 = {},
     -- some extra state to compute selection between mouse press and release
@@ -102,22 +102,19 @@ function edit.draw(State)
   State.cursor_x = nil
   State.cursor_y = nil
   local y = State.top
-  local screen_bottom1 = {line=nil, pos=nil}
 --?   print('== draw')
   for line_index = State.screen_top1.line,#State.lines do
     local line = State.lines[line_index]
 --?     print('draw:', y, line_index, line)
     if y + State.line_height > App.screen.height then break end
-    screen_bottom1.line = line_index
 --?     print('text.draw', y, line_index)
     local startpos = 1
     if line_index == State.screen_top1.line then
       startpos = State.screen_top1.pos
     end
-    y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
+    y = Text.draw(State, line_index, y, startpos)
 --?     print('=> y', y)
   end
-  State.screen_bottom1 = screen_bottom1
   if State.search_term then
     Text.draw_search_bar(State)
   end
@@ -173,10 +170,7 @@ function edit.mouse_press(State, x,y, mouse_button)
   State.old_cursor1 = State.cursor1
   State.old_selection1 = State.selection1
   State.mousepress_shift = App.shift_down()
-  State.selection1 = {
-      line=State.screen_bottom1.line,
-      pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
-  }
+  State.selection1 = Text.final_text_loc_on_screen(State)
 end
 
 function edit.mouse_release(State, x,y, mouse_button)
@@ -202,7 +196,7 @@ function edit.mouse_release(State, x,y, mouse_button)
   end
 
   -- still here? mouse release is below all screen lines
-  State.cursor1.line, State.cursor1.pos = State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
+  State.cursor1 = Text.final_text_loc_on_screen(State)
   edit.clean_up_mouse_press(State)
 --?   print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
 end
@@ -228,7 +222,8 @@ function edit.mouse_wheel_move(State, dx,dy)
       Text.up(State)
     end
   elseif dy < 0 then
-    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
+    State.cursor1 = Text.screen_bottom1(State)
+    edit.put_cursor_on_next_text_line(State)
     for i=1,math.floor(-dy) do
       Text.down(State)
     end
@@ -252,7 +247,6 @@ function edit.keychord_press(State, chord, key)
     Text.delete_selection(State, State.left, State.right)
   end
   if State.search_term then
-    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end  -- just in case we scroll
     if chord == 'escape' then
       State.search_term = nil
       State.cursor1 = State.search_backup.cursor
@@ -302,7 +296,6 @@ function edit.keychord_press(State, chord, key)
     end
   -- dispatch to text
   else
-    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end  -- just in case we scroll
     Text.keychord_press(State, chord)
   end
 end
@@ -357,6 +350,7 @@ end
 function edit.run_after_mouse_click(State, x,y, mouse_button)
   App.fake_mouse_press(x,y, mouse_button)
   edit.mouse_press(State, x,y, mouse_button)
+  edit.draw(State)
   App.fake_mouse_release(x,y, mouse_button)
   edit.mouse_release(State, x,y, mouse_button)
   App.screen.contents = {}
diff --git a/help.lua b/help.lua
index 6f8633b..cc7a0c9 100644
--- a/help.lua
+++ b/help.lua
@@ -1,8 +1,8 @@
 function draw_help_without_mouse_pressed(State, drawing_index)
   local drawing = State.lines[drawing_index]
-  local line_cache = State.line_cache[drawing_index]
+  local starty = Text.starty(State, drawing_index)
   App.color(Help_color)
-  local y = line_cache.starty+10
+  local y = starty+10
   love.graphics.print("Things you can do:", State.left+30,y)
   y = y + State.line_height
   love.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y)
@@ -48,14 +48,14 @@ function draw_help_without_mouse_pressed(State, drawing_index)
   love.graphics.print("Press 'esc' now to hide this message", State.left+30,y)
   y = y + State.line_height
   App.color(Help_background_color)
-  love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))
+  love.graphics.rectangle('fill', State.left,starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-starty))
 end
 
 function draw_help_with_mouse_pressed(State, drawing_index)
   local drawing = State.lines[drawing_index]
-  local line_cache = State.line_cache[drawing_index]
+  local starty = Text.starty(State, drawing_index)
   App.color(Help_color)
-  local y = line_cache.starty+10
+  local y = starty+10
   love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y)
   y = y + State.line_height
   love.graphics.print('Things you can do now:', State.left+30,y)
@@ -129,7 +129,7 @@ function draw_help_with_mouse_pressed(State, drawing_index)
     y = y + State.line_height
   end
   App.color(Help_background_color)
-  love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))
+  love.graphics.rectangle('fill', State.left,starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-starty))
 end
 
 function current_shape(State, shape)
diff --git a/search.lua b/search.lua
index 54bab14..d3a5fea 100644
--- a/search.lua
+++ b/search.lua
@@ -62,7 +62,8 @@ function Text.search_next(State)
     State.screen_top1.line = State.search_backup.screen_top.line
     State.screen_top1.pos = State.search_backup.screen_top.pos
   end
-  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(State.screen_bottom1, State.cursor1) then
+  local screen_bottom1 = Text.screen_bottom1(State)
+  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
     State.screen_top1.line = State.cursor1.line
     local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
     State.screen_top1.pos = pos
@@ -115,7 +116,8 @@ function Text.search_previous(State)
     State.screen_top1.line = State.search_backup.screen_top.line
     State.screen_top1.pos = State.search_backup.screen_top.pos
   end
-  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(State.screen_bottom1, State.cursor1) then
+  local screen_bottom1 = Text.screen_bottom1(State)
+  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
     State.screen_top1.line = State.cursor1.line
     local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
     State.screen_top1.pos = pos
diff --git a/select.lua b/select.lua
index 79eeb82..2d0851a 100644
--- a/select.lua
+++ b/select.lua
@@ -69,7 +69,7 @@ end
 
 function Text.mouse_pos(State)
   local x,y = App.mouse_x(), App.mouse_y()
-  if y < State.line_cache[State.screen_top1.line].starty then
+  if y < State.top then
     return State.screen_top1.line, State.screen_top1.pos
   end
   for line_index,line in ipairs(State.lines) do
@@ -77,7 +77,8 @@ function Text.mouse_pos(State)
       return line_index, Text.to_pos_on_line(State, line_index, x,y)
     end
   end
-  return State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
+  local screen_bottom1 = Text.screen_bottom1(State)
+  return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1)
 end
 
 function Text.selection(State)
diff --git a/source.lua b/source.lua
index 9bc400e..7eec594 100644
--- a/source.lua
+++ b/source.lua
@@ -307,7 +307,6 @@ function source.mouse_press(x,y, mouse_button)
       return
     end
     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
 
diff --git a/source_edit.lua b/source_edit.lua
index e376537..5351857 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -33,7 +33,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
     --    string data,
     -- a drawing is a table with:
     --    mode = 'drawing'
-    --    a (y) coord in pixels (updated while painting screen),
     --    a (h)eight,
     --    an array of points, and
     --    an array of shapes
@@ -54,7 +53,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
 
     -- rendering wrapped text lines needs some additional short-lived data per line:
     --   startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen
-    --   starty, the y coord in pixels the line starts rendering from
     --   fragments: snippets of the line guaranteed to not straddle screen lines
     --   screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
     line_cache = {},
@@ -68,9 +66,10 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
     --
     -- Make sure these coordinates are never aliased, so that changing one causes
     -- action at a distance.
+    --
+    -- On lines that are drawings, pos will be nil.
     screen_top1 = {line=1, pos=1},  -- position of start of screen line at top of screen
-    cursor1 = {line=1, pos=1},  -- position of cursor
-    screen_bottom1 = {line=1, pos=1},  -- position of start of screen line at bottom of screen
+    cursor1 = {line=1, pos=1},  -- position of cursor; must be on a text line
 
     selection1 = {},
     -- some extra state to compute selection between mouse press and release
@@ -165,13 +164,11 @@ function edit.draw(State, hide_cursor, show_line_numbers)
   State.cursor_x = nil
   State.cursor_y = nil
   local y = State.top
-  local screen_bottom1 = {line=nil, pos=nil}
 --?   print('== draw')
   for line_index = State.screen_top1.line,#State.lines do
     local line = State.lines[line_index]
 --?     print('draw:', y, line_index, line)
     if y + State.line_height > App.screen.height then break end
-    screen_bottom1.line = line_index
     if line.mode == 'text' then
 --?       print('text.draw', y, line_index)
       local startpos = 1
@@ -198,7 +195,7 @@ function edit.draw(State, hide_cursor, show_line_numbers)
                      end,
         })
       end
-      y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos, hide_cursor, show_line_numbers)
+      y = Text.draw(State, line_index, y, startpos, hide_cursor, show_line_numbers)
 --?       print('=> y', y)
     elseif line.mode == 'drawing' then
       y = y+Drawing_padding_top
@@ -208,7 +205,6 @@ function edit.draw(State, hide_cursor, show_line_numbers)
       assert(false, ('unknown line mode %s'):format(line.mode))
     end
   end
-  State.screen_bottom1 = screen_bottom1
   if State.search_term then
     Text.draw_search_bar(State)
   end
@@ -281,8 +277,7 @@ function edit.mouse_press(State, x,y, mouse_button)
         return
       end
     elseif line.mode == 'drawing' then
-      local line_cache = State.line_cache[line_index]
-      if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
+      if Drawing.in_drawing(State, line_index, x, y, State.left,State.right) then
         State.lines.current_drawing_index = line_index
         State.lines.current_drawing = line
         Drawing.before = snapshot(State, line_index)
@@ -296,10 +291,7 @@ function edit.mouse_press(State, x,y, mouse_button)
   State.old_cursor1 = State.cursor1
   State.old_selection1 = State.selection1
   State.mousepress_shift = App.shift_down()
-  State.selection1 = {
-      line=State.screen_bottom1.line,
-      pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
-  }
+  State.selection1 = Text.final_text_loc_on_screen(State)
 end
 
 function edit.mouse_release(State, x,y, mouse_button)
@@ -337,7 +329,7 @@ function edit.mouse_release(State, x,y, mouse_button)
     end
 
     -- still here? mouse release is below all screen lines
-    State.cursor1.line, State.cursor1.pos = State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
+    State.cursor1 = Text.final_text_loc_on_screen(State)
     edit.clean_up_mouse_press(State)
 --?     print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
   end
@@ -365,7 +357,7 @@ function edit.mouse_wheel_move(State, dx,dy)
       Text.up(State)
     end
   elseif dy < 0 then
-    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
+    State.cursor1 = Text.screen_bottom1(State)
     edit.put_cursor_on_next_text_line(State)
     for i=1,math.floor(-dy) do
       Text.down(State)
@@ -399,11 +391,10 @@ function edit.keychord_press(State, chord, key)
       -- printable character created using shift key => delete selection
       -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
       (not App.shift_down() or utf8.len(key) == 1) and
-      chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(chord) then
+      chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(key) then
     Text.delete_selection(State, State.left, State.right)
   end
   if State.search_term then
-    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end  -- just in case we scroll
     if chord == 'escape' then
       State.search_term = nil
       State.cursor1 = State.search_backup.cursor
@@ -508,7 +499,6 @@ function edit.keychord_press(State, chord, key)
     record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
   -- dispatch to drawing or text
   elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
-    -- DON'T reset line_cache.starty here
     local drawing_index, drawing = Drawing.current_drawing(State)
     if drawing_index then
       local before = snapshot(State, drawing_index)
@@ -545,7 +535,6 @@ function edit.keychord_press(State, chord, key)
     end
     schedule_save(State)
   else
-    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end  -- just in case we scroll
     Text.keychord_press(State, chord)
   end
 end
@@ -600,6 +589,7 @@ end
 function edit.run_after_mouse_click(State, x,y, mouse_button)
   App.fake_mouse_press(x,y, mouse_button)
   edit.mouse_press(State, x,y, mouse_button)
+  edit.draw(State)
   App.fake_mouse_release(x,y, mouse_button)
   edit.mouse_release(State, x,y, mouse_button)
   App.screen.contents = {}
diff --git a/source_select.lua b/source_select.lua
index 6e21c7b..b67dd16 100644
--- a/source_select.lua
+++ b/source_select.lua
@@ -69,7 +69,7 @@ end
 
 function Text.mouse_pos(State)
   local x,y = App.mouse_x(), App.mouse_y()
-  if y < State.line_cache[State.screen_top1.line].starty then
+  if y < State.top then
     return State.screen_top1.line, State.screen_top1.pos
   end
   for line_index,line in ipairs(State.lines) do
@@ -79,7 +79,8 @@ function Text.mouse_pos(State)
       end
     end
   end
-  return State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
+  local screen_bottom1 = Text.screen_bottom1(State)
+  return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1)
 end
 
 function Text.cut_selection(State)
diff --git a/source_text.lua b/source_text.lua
index 7c1838c..6e0c4f9 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -2,14 +2,12 @@
 Text = {}
 
 -- draw a line starting from startpos to screen at y between State.left and State.right
--- return y for the next line, and position of start of final screen line drawn
+-- return y for the next line
 function Text.draw(State, line_index, y, startpos, hide_cursor, show_line_numbers)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  line_cache.starty = y
   line_cache.startpos = startpos
   -- wrap long lines
-  local final_screen_line_starting_pos = startpos  -- track value to return
   Text.populate_screen_line_starting_pos(State, line_index)
   Text.populate_link_offsets(State, line_index)
   if show_line_numbers then
@@ -24,7 +22,6 @@ function Text.draw(State, line_index, y, startpos, hide_cursor, show_line_number
       -- render nothing
 --?       print('skipping', screen_line)
     else
-      final_screen_line_starting_pos = pos
       local screen_line = Text.screen_line(line, line_cache, i)
 --?       print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
       local frag_len = utf8.len(screen_line)
@@ -84,7 +81,7 @@ function Text.draw(State, line_index, y, startpos, hide_cursor, show_line_number
       end
     end
   end
-  return y, final_screen_line_starting_pos
+  return y
 end
 
 function Text.screen_line(line, line_cache, i)
@@ -208,7 +205,7 @@ function Text.text_input(State, t)
     end
   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)
+--?   print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.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)
@@ -241,12 +238,12 @@ function Text.keychord_press(State, chord)
     record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
   elseif chord == 'tab' then
     local before = snapshot(State, State.cursor1.line)
---?     print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?     print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
     Text.insert_at_cursor(State, '\t')
     if State.cursor_y > App.screen.height - State.line_height then
       Text.populate_screen_line_starting_pos(State, State.cursor1.line)
       Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
---?       print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?       print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
     end
     schedule_save(State)
     record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
@@ -427,37 +424,79 @@ function Text.insert_return(State)
 end
 
 function Text.pageup(State)
---?   print('pageup')
+  State.screen_top1 = Text.previous_screen_top1(State)
+  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
+  Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
+  Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
+end
+
+-- return the top y coordinate of a given line_index,
+-- or nil if no part of it is on screen
+function Text.starty(State, line_index)
+  -- duplicate some logic from love.draw
+  -- does not modify State (except to populate line_cache)
+  if line_index < State.screen_top1.line then return end
+  local loc2 = Text.to2(State, State.screen_top1)
+  local y = State.top
+  while true do
+    if State.lines[loc2.line].mode == 'drawing' then
+      y = y + Drawing_padding_top
+    end
+    if loc2.line == line_index then return y end
+    if State.lines[loc2.line].mode == 'text' then
+      y = y + State.line_height
+    elseif State.lines[loc2.line].mode == 'drawing' then
+      y = y + Drawing.pixels(State.lines[loc2.line].h, State.width) + Drawing_padding_bottom
+    end
+    if y + State.line_height > App.screen.height then break end
+    local next_loc2 = Text.next_screen_line(State, loc2)
+    if Text.eq2(next_loc2, loc2) then break end  -- end of file
+    loc2 = next_loc2
+  end
+end
+
+function Text.previous_screen_top1(State)
   -- duplicate some logic from love.draw
-  local top2 = Text.to2(State, State.screen_top1)
---?   print(App.screen.height)
+  -- does not modify State (except to populate line_cache)
+  local loc2 = Text.to2(State, State.screen_top1)
   local y = App.screen.height - State.line_height
   while y >= State.top do
---?     print(y, top2.line, top2.screen_line, top2.screen_pos)
-    if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end
-    if State.lines[State.screen_top1.line].mode == 'text' then
+    if loc2.line == 1 and loc2.screen_line == 1 and loc2.screen_pos == 1 then break end
+    if State.lines[loc2.line].mode == 'text' then
       y = y - State.line_height
-    elseif State.lines[State.screen_top1.line].mode == 'drawing' then
-      y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
+    elseif State.lines[loc2.line].mode == 'drawing' then
+      y = y - Drawing_padding_height - Drawing.pixels(State.lines[loc2.line].h, State.width)
     end
-    top2 = Text.previous_screen_line(State, top2)
+    loc2 = Text.previous_screen_line(State, loc2)
   end
-  State.screen_top1 = Text.to1(State, top2)
-  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-  Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
---?   print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
---?   print('pageup end')
+  return Text.to1(State, loc2)
 end
 
 function Text.pagedown(State)
---?   print('pagedown')
-  State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
---?   print('setting top to', State.screen_top1.line, State.screen_top1.pos)
+  State.screen_top1 = Text.screen_bottom1(State)
   State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
   Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
---?   print('top now', State.screen_top1.line)
   Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
---?   print('pagedown end')
+end
+
+-- return the location of the start of the bottom-most line on screen
+function Text.screen_bottom1(State)
+  -- duplicate some logic from love.draw
+  -- does not modify State (except to populate line_cache)
+  local loc2 = Text.to2(State, State.screen_top1)
+  local y = State.top
+  while true do
+    if State.lines[loc2.line].mode == 'text' then
+      y = y + State.line_height
+    elseif State.lines[loc2.line].mode == 'drawing' then
+      y = y + Drawing_padding_height + Drawing.pixels(State.lines[loc2.line].h, State.width)
+    end
+    if y + State.line_height > App.screen.height then break end
+    local next_loc2 = Text.next_screen_line(State, loc2)
+    if Text.eq2(next_loc2, loc2) then break end
+    loc2 = next_loc2
+  end
+  return Text.to1(State, loc2)
 end
 
 function Text.up(State)
@@ -505,7 +544,7 @@ end
 
 function Text.down(State)
   assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
---?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   assert(State.cursor1.pos, 'cursor has no pos')
   if Text.cursor_at_final_screen_line(State) then
     -- line is done, skip to next text line
@@ -522,7 +561,9 @@ function Text.down(State)
         break
       end
     end
-    if State.cursor1.line > State.screen_bottom1.line then
+    local screen_bottom1 = Text.screen_bottom1(State)
+--?   print('down 2', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, screen_bottom1.line, screen_bottom1.pos)
+    if State.cursor1.line > screen_bottom1.line then
 --?       print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
 --?       print('scroll up preserving cursor')
       Text.snap_cursor_to_bottom_of_screen(State)
@@ -530,7 +571,8 @@ function Text.down(State)
     end
   else
     -- move down one screen line in current line
-    local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
+    local screen_bottom1 = Text.screen_bottom1(State)
+    local scroll_down = Text.le1(screen_bottom1, State.cursor1)
 --?     print('cursor is NOT at final screen line of its line')
     local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
     Text.populate_screen_line_starting_pos(State, State.cursor1.line)
@@ -546,7 +588,7 @@ function Text.down(State)
 --?       print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
     end
   end
---?   print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
 end
 
 function Text.start_of_line(State)
@@ -683,6 +725,7 @@ function Text.pos_at_start_of_screen_line(State, loc1)
 end
 
 function Text.pos_at_end_of_screen_line(State, loc1)
+  assert(State.lines[loc1.line].mode == 'text')
   Text.populate_screen_line_starting_pos(State, loc1.line)
   local line_cache = State.line_cache[loc1.line]
   local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
@@ -696,6 +739,25 @@ function Text.pos_at_end_of_screen_line(State, loc1)
   assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
+function Text.final_text_loc_on_screen(State)
+  local screen_bottom1 = Text.screen_bottom1(State)
+  if State.lines[screen_bottom1.line].mode == 'text' then
+    return {
+      line=screen_bottom1.line,
+      pos=Text.pos_at_end_of_screen_line(State, screen_bottom1),
+    }
+  end
+  local loc2 = Text.to2(State, screen_bottom1)
+  while true do
+    if State.lines[loc2.line].mode == 'text' then break end
+    assert(loc2.line > 1 or loc2.screen_line > 1 and loc2.screen_pos > 1)  -- elsewhere we're making sure there's always at least one text line on screen
+    loc2 = Text.previous_screen_line(State, loc2)
+  end
+  local result = Text.to1(State, loc2)
+  result.pos = Text.pos_at_end_of_screen_line(State, result)
+  return result
+end
+
 function Text.cursor_at_final_screen_line(State)
   Text.populate_screen_line_starting_pos(State, State.cursor1.line)
   local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos
@@ -736,7 +798,7 @@ function Text.snap_cursor_to_bottom_of_screen(State)
 --?   print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
   -- slide to start of screen line
   top2.screen_pos = 1  -- start of screen line
---?   print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
 --?   print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
   local y = App.screen.height - State.line_height
   -- duplicate some logic from love.draw
@@ -766,26 +828,28 @@ function Text.snap_cursor_to_bottom_of_screen(State)
 --?   print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
   State.screen_top1 = Text.to1(State, top2)
 --?   print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
---?   print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
   Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
 end
 
 function Text.in_line(State, line_index, x,y)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  if line_cache.starty == nil then return false end  -- outside current page
-  if y < line_cache.starty then return false end
+  local starty = Text.starty(State, line_index)
+  if starty == nil then return false end  -- outside current page
+  if y < starty then return false end
   Text.populate_screen_line_starting_pos(State, line_index)
-  return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
+  return y < starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
 end
 
 -- convert mx,my in pixels to schema-1 coordinates
 function Text.to_pos_on_line(State, line_index, mx, my)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  assert(my >= line_cache.starty, 'failed to map y pixel to line')
+  local starty = Text.starty(State, line_index)
+  assert(my >= starty, 'failed to map y pixel to line')
   -- duplicate some logic from Text.draw
-  local y = line_cache.starty
+  local y = starty
   local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
   for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
     local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
@@ -971,6 +1035,10 @@ function Text.le1(a, b)
   return a.pos <= b.pos
 end
 
+function Text.eq2(a, b)
+  return a.line == b.line and a.screen_line == b.screen_line and a.screen_pos == b.screen_pos
+end
+
 function Text.offset(s, pos1)
   if pos1 == 1 then return 1 end
   local result = utf8.offset(s, pos1)
@@ -994,6 +1062,22 @@ function Text.previous_screen_line(State, loc2)
   end
 end
 
+function Text.next_screen_line(State, loc2)
+  if State.lines[loc2.line].mode == 'drawing' then
+    return {line=loc2.line+1, screen_line=1, screen_pos=1}
+  end
+  Text.populate_screen_line_starting_pos(State, loc2.line)
+  if loc2.screen_line >= #State.line_cache[loc2.line].screen_line_starting_pos then
+    if loc2.line < #State.lines then
+      return {line=loc2.line+1, screen_line=1, screen_pos=1}
+    else
+      return loc2
+    end
+  else
+    return {line=loc2.line, screen_line=loc2.screen_line+1, screen_pos=1}
+  end
+end
+
 -- resize helper
 function Text.tweak_screen_top_and_cursor(State)
   if State.screen_top1.pos == 1 then return end
@@ -1017,16 +1101,12 @@ function Text.tweak_screen_top_and_cursor(State)
     end
   end
   -- make sure cursor is on screen
+  local screen_bottom1 = Text.screen_bottom1(State)
   if Text.lt1(State.cursor1, State.screen_top1) then
     State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-  elseif State.cursor1.line >= State.screen_bottom1.line then
---?     print('too low')
+  elseif State.cursor1.line >= screen_bottom1.line then
     if Text.cursor_out_of_screen(State) then
---?       print('tweak')
-      State.cursor1 = {
-          line=State.screen_bottom1.line,
-          pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),
-      }
+      State.cursor1 = Text.final_text_loc_on_screen(State)
     end
   end
 end
@@ -1035,11 +1115,6 @@ end
 function Text.cursor_out_of_screen(State)
   edit.draw(State)
   return State.cursor_y == nil
-  -- this approach is cheaper and almost works, except on the final screen
-  -- where file ends above bottom of screen
---?   local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
---?   local botline1 = {line=State.cursor1.line, pos=botpos}
---?   return Text.lt1(State.screen_bottom1, botline1)
 end
 
 function Text.redraw_all(State)
diff --git a/source_text_tests.lua b/source_text_tests.lua
index 6376ec8..11cc823 100644
--- a/source_text_tests.lua
+++ b/source_text_tests.lua
@@ -16,7 +16,7 @@ function test_initial_state()
 end
 
 function test_click_to_create_drawing()
-  App.screen.init{width=120, height=60}
+  App.screen.init{width=800, height=600}
   Editor_state = edit.initialize_test_state()
   Editor_state.lines = load_array{}
   Text.redraw_all(Editor_state)
@@ -75,7 +75,6 @@ function test_press_ctrl()
   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', 'm')
 end
 
@@ -232,7 +231,7 @@ function test_skip_multiple_spaces_to_next_word()
   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',  'right')
+  edit.run_after_keychord(Editor_state, 'M-right', 'right')
   check_eq(Editor_state.cursor1.pos, 9, 'check')
 end
 
@@ -243,7 +242,7 @@ function test_move_past_end_of_word_on_next_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=8}
   edit.draw(Editor_state)
-  edit.run_after_keychord(Editor_state, 'M-right',  'right')
+  edit.run_after_keychord(Editor_state, 'M-right', 'right')
   check_eq(Editor_state.cursor1.line, 2, 'line')
   check_eq(Editor_state.cursor1.pos, 4, 'pos')
 end
@@ -255,9 +254,8 @@ function test_click_moves_cursor()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
@@ -274,7 +272,6 @@ function test_click_to_left_of_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=3}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click to the left of the line
   edit.draw(Editor_state)
@@ -294,7 +291,6 @@ function test_click_takes_margins_into_account()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click on the other line
   edit.draw(Editor_state)
@@ -313,7 +309,6 @@ function test_click_on_empty_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click on the empty line
   edit.draw(Editor_state)
@@ -332,7 +327,6 @@ function test_click_below_all_lines()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click below first line
   edit.draw(Editor_state)
@@ -350,7 +344,6 @@ function test_draw_text()
   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', 'screen:1')
@@ -367,7 +360,6 @@ function test_draw_wrapping_text()
   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', 'screen:1')
@@ -384,7 +376,6 @@ function test_draw_word_wrapping_text()
   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 ', 'screen:1')
@@ -402,7 +393,6 @@ function test_click_on_wrapping_line()
   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)
@@ -421,7 +411,6 @@ function test_click_on_wrapping_line_takes_margins_into_account()
   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)
@@ -439,7 +428,6 @@ function test_draw_text_wrapping_within_word()
   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 ', 'screen:1')
@@ -457,7 +445,6 @@ function test_draw_wrapping_text_containing_non_ascii()
   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', 'screen:1')
@@ -476,7 +463,6 @@ function test_click_past_end_of_screen_line()
   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 ', 'baseline/screen:1')
@@ -499,7 +485,6 @@ function test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()
   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", 'baseline/screen:2')
@@ -520,7 +505,6 @@ function test_click_past_end_of_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -544,7 +528,6 @@ function test_click_past_end_of_wrapping_line_containing_non_ascii()
   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 ', 'baseline/screen:1')
@@ -569,7 +552,6 @@ function test_click_past_end_of_word_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -588,11 +570,10 @@ function test_select_text()
   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)
   -- select a letter
   App.fake_key_press('lshift')
-  edit.run_after_keychord(Editor_state, 'S-right',  'right')
+  edit.run_after_keychord(Editor_state, 'S-right', 'right')
   App.fake_key_release('lshift')
   edit.key_release(Editor_state, 'lshift')
   -- selection persists even after shift is released
@@ -611,10 +592,9 @@ function test_cursor_movement_without_shift_resets_selection()
   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',  'right')
+  edit.run_after_keychord(Editor_state, 'right', 'right')
   -- no change to data, selection is reset
   check_nil(Editor_state.selection1.line, 'check')
   check_eq(Editor_state.lines[1].data, 'abc', 'data')
@@ -629,7 +609,6 @@ function test_edit_deletes_selection()
   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_text_input(Editor_state, 'x')
@@ -646,7 +625,6 @@ function test_edit_with_shift_key_deletes_selection()
   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')
@@ -668,7 +646,6 @@ function test_copy_does_not_reset_selection()
   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', 'c')
@@ -686,7 +663,6 @@ function test_cut()
   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', 'x')
@@ -704,7 +680,6 @@ function test_paste_replaces_selection()
   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'
@@ -723,7 +698,6 @@ function test_deleting_selection_may_scroll()
   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', 'baseline/screen:1')
@@ -747,7 +721,6 @@ function test_edit_wrapping_text()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=4}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   edit.run_after_text_input(Editor_state, 'g')
   local y = Editor_state.top
@@ -766,7 +739,6 @@ function test_insert_newline()
   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', 'baseline/screen:1')
@@ -795,7 +767,6 @@ function test_insert_newline_at_start_of_line()
   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', 'return')
   check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
@@ -812,7 +783,6 @@ function test_insert_from_clipboard()
   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', 'baseline/screen:1')
@@ -841,9 +811,8 @@ function test_select_text_using_mouse()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- press and hold on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   -- drag and release somewhere else
@@ -861,9 +830,8 @@ function test_select_text_using_mouse_starting_above_text()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- press mouse above first line of text
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
   check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
@@ -879,7 +847,6 @@ function test_select_text_using_mouse_starting_above_text_wrapping_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=5}
   Editor_state.screen_top1 = {line=2, pos=3}
-  Editor_state.screen_bottom1 = {}
   -- press mouse above first line of text
   edit.draw(Editor_state)
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
@@ -902,7 +869,6 @@ function test_select_text_using_mouse_starting_below_text()
   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, 'ab', 'baseline:screen:1')
@@ -923,9 +889,8 @@ function test_select_text_using_mouse_and_shift()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- click on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
@@ -948,9 +913,8 @@ function test_select_text_repeatedly_using_mouse_and_shift()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- click on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
@@ -978,7 +942,6 @@ function test_select_all_text()
   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)
   -- select all
   App.fake_key_press('lctrl')
@@ -1000,7 +963,6 @@ function test_cut_without_selection()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=2}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   edit.draw(Editor_state)
   -- try to cut without selecting text
@@ -1016,7 +978,6 @@ function test_pagedown()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- initially the first two lines are displayed
   edit.draw(Editor_state)
   local y = Editor_state.top
@@ -1046,7 +1007,6 @@ function test_pagedown_skips_drawings()
   check_eq(Editor_state.lines[2].mode, 'drawing', 'baseline/lines')
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   local drawing_height = Drawing_padding_height + drawing_width/2  -- default
   -- initially the screen displays the first line and the drawing
   -- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80px
@@ -1070,7 +1030,6 @@ function test_pagedown_can_start_from_middle_of_long_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -1105,7 +1064,6 @@ function test_pagedown_never_moves_up()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=9}
   Editor_state.screen_top1 = {line=1, pos=9}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- pagedown makes no change
   edit.run_after_keychord(Editor_state, 'pagedown', 'pagedown')
@@ -1120,7 +1078,6 @@ function test_down_arrow_moves_cursor()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- initially the first three lines are displayed
   edit.draw(Editor_state)
   local y = Editor_state.top
@@ -1153,7 +1110,6 @@ function test_down_arrow_skips_drawing()
   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', 'baseline/screen:1')
@@ -1175,7 +1131,6 @@ function test_down_arrow_scrolls_down_by_one_line()
   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', 'baseline/screen:1')
@@ -1203,7 +1158,6 @@ function test_down_arrow_scrolls_down_by_one_screen_line()
   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', 'baseline/screen:1')
@@ -1232,7 +1186,6 @@ function test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_
   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', 'baseline/screen:1')
@@ -1260,7 +1213,6 @@ function test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up()
   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', 'baseline/screen:1')
@@ -1294,7 +1246,6 @@ function test_up_arrow_moves_cursor()
   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', 'baseline/screen:1')
@@ -1326,7 +1277,6 @@ function test_up_arrow_skips_drawing()
   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', 'baseline/screen:1')
@@ -1348,7 +1298,6 @@ function test_up_arrow_scrolls_up_by_one_line()
   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', 'baseline/screen:1')
@@ -1376,7 +1325,6 @@ function test_up_arrow_scrolls_up_by_one_line_skipping_drawing()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=3, pos=1}
   Editor_state.screen_top1 = {line=3, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   local y = Editor_state.top
   App.screen.check(y, 'def', 'baseline/screen:1')
@@ -1398,7 +1346,6 @@ function test_up_arrow_scrolls_up_by_one_screen_line()
   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', 'baseline/screen:1')
@@ -1426,7 +1373,6 @@ function test_up_arrow_scrolls_up_to_final_screen_line()
   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', 'baseline/screen:1')
@@ -1456,7 +1402,6 @@ function test_up_arrow_scrolls_up_to_empty_line()
   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', 'baseline/screen:1')
@@ -1483,7 +1428,6 @@ function test_pageup()
   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
@@ -1508,7 +1452,6 @@ function test_pageup_scrolls_up_by_screen_line()
   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', 'baseline/screen:1')
@@ -1537,7 +1480,6 @@ function test_pageup_scrolls_up_from_middle_screen_line()
   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', 'baseline/screen:2')
@@ -1564,7 +1506,6 @@ function test_enter_on_bottom_line_scrolls_down()
   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', 'baseline/screen:1')
@@ -1593,7 +1534,6 @@ function test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()
   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', 'baseline/screen:1')
@@ -1616,7 +1556,6 @@ function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bot
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=2, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- after hitting the inserting_text key the screen does not scroll down
   edit.run_after_text_input(Editor_state, 'a')
@@ -1635,7 +1574,6 @@ function test_typing_on_bottom_line_scrolls_down()
   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', 'baseline/screen:1')
@@ -1665,7 +1603,6 @@ function test_left_arrow_scrolls_up_in_wrapped_line()
   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)
@@ -1694,7 +1631,6 @@ function test_right_arrow_scrolls_down_in_wrapped_line()
   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)
@@ -1705,7 +1641,7 @@ function test_right_arrow_scrolls_down_in_wrapped_line()
   y = y + Editor_state.line_height
   App.screen.check(y, 'ghi ', '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',  'right')
+  edit.run_after_keychord(Editor_state, 'right', 'right')
   check_eq(Editor_state.screen_top1.line, 2, 'screen_top')
   check_eq(Editor_state.cursor1.line, 3, 'cursor:line')
   check_eq(Editor_state.cursor1.pos, 6, 'cursor:pos')
@@ -1724,7 +1660,6 @@ function test_home_scrolls_up_in_wrapped_line()
   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)
@@ -1753,7 +1688,6 @@ function test_end_scrolls_down_in_wrapped_line()
   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)
@@ -1784,7 +1718,6 @@ function test_position_cursor_on_recently_edited_wrapping_line()
   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 ', 'baseline1/screen:1')
@@ -1818,7 +1751,6 @@ function test_backspace_can_scroll_up()
   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', 'baseline/screen:1')
@@ -1846,7 +1778,6 @@ function test_backspace_can_scroll_up_screen_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=3, pos=5}
   Editor_state.screen_top1 = {line=3, pos=5}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   local y = Editor_state.top
   App.screen.check(y, 'jkl', 'baseline/screen:1')
@@ -1981,7 +1912,6 @@ function test_undo_insert_text()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=4}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- insert a character
   edit.draw(Editor_state)
   edit.run_after_text_input(Editor_state, 'g')
@@ -2016,7 +1946,6 @@ function test_undo_delete_text()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=5}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- delete a character
   edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
   check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line')
@@ -2055,7 +1984,6 @@ function test_undo_restores_selection()
   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)
   -- delete selected text
   edit.run_after_text_input(Editor_state, 'x')
@@ -2076,7 +2004,6 @@ function test_search()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -2103,7 +2030,6 @@ function test_search_upwards()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -2121,7 +2047,6 @@ function test_search_wrap()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -2139,7 +2064,6 @@ function test_search_wrap_upwards()
   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', 'f')
diff --git a/text.lua b/text.lua
index ad86378..ec28509 100644
--- a/text.lua
+++ b/text.lua
@@ -2,15 +2,13 @@
 Text = {}
 
 -- draw a line starting from startpos to screen at y between State.left and State.right
--- return y for the next line, and position of start of final screen line drawn
+-- return y for the next line
 function Text.draw(State, line_index, y, startpos)
 --?   print('text.draw', line_index, y)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  line_cache.starty = y
   line_cache.startpos = startpos
   -- wrap long lines
-  local final_screen_line_starting_pos = startpos  -- track value to return
   Text.populate_screen_line_starting_pos(State, line_index)
   assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info')
   for i=1,#line_cache.screen_line_starting_pos do
@@ -18,7 +16,6 @@ function Text.draw(State, line_index, y, startpos)
     if pos < startpos then
       -- render nothing
     else
-      final_screen_line_starting_pos = pos
       local screen_line = Text.screen_line(line, line_cache, i)
 --?       print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
       local frag_len = utf8.len(screen_line)
@@ -57,7 +54,7 @@ function Text.draw(State, line_index, y, startpos)
       end
     end
   end
-  return y, final_screen_line_starting_pos
+  return y
 end
 
 function Text.screen_line(line, line_cache, i)
@@ -226,45 +223,64 @@ function Text.keychord_press(State, chord)
 end
 
 function Text.pageup(State)
---?   print('pageup')
+  State.screen_top1 = Text.previous_screen_top1(State)
+  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
+  Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
+  Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
+end
+
+-- return the top y coordinate of a given line_index,
+-- or nil if no part of it is on screen
+function Text.starty(State, line_index)
+  -- duplicate some logic from love.draw
+  -- does not modify State (except to populate line_cache)
+  if line_index < State.screen_top1.line then return end
+  local loc2 = Text.to2(State, State.screen_top1)
+  local y = State.top
+  while true do
+    if loc2.line == line_index then return y end
+    y = y + State.line_height
+    if y + State.line_height > App.screen.height then break end
+    local next_loc2 = Text.next_screen_line(State, loc2)
+    if Text.eq2(next_loc2, loc2) then break end  -- end of file
+    loc2 = next_loc2
+  end
+end
+
+function Text.previous_screen_top1(State)
   -- duplicate some logic from love.draw
-  local top2 = Text.to2(State, State.screen_top1)
---?   print(App.screen.height)
+  -- does not modify State (except to populate line_cache)
+  local loc2 = Text.to2(State, State.screen_top1)
   local y = App.screen.height - State.line_height
   while y >= State.top do
---?     print(y, top2.line, top2.screen_line, top2.screen_pos)
-    if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end
+    if loc2.line == 1 and loc2.screen_line == 1 and loc2.screen_pos == 1 then break end
     y = y - State.line_height
-    top2 = Text.previous_screen_line(State, top2)
+    loc2 = Text.previous_screen_line(State, loc2)
   end
-  State.screen_top1 = Text.to1(State, top2)
-  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-  Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
---?   print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
---?   print('pageup end')
+  return Text.to1(State, loc2)
 end
 
 function Text.pagedown(State)
---?   print('pagedown')
-  -- If a line/paragraph gets to a page boundary, I often want to scroll
-  -- before I get to the bottom.
-  -- However, only do this if it makes forward progress.
-  local bot2 = Text.to2(State, State.screen_bottom1)
-  if bot2.screen_line > 1 then
-    bot2.screen_line = math.max(bot2.screen_line-10, 1)
-  end
-  local new_top1 = Text.to1(State, bot2)
-  if Text.lt1(State.screen_top1, new_top1) then
-    State.screen_top1 = new_top1
-  else
-    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
-  end
---?   print('setting top to', State.screen_top1.line, State.screen_top1.pos)
+  State.screen_top1 = Text.screen_bottom1(State)
   State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
   Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
---?   print('top now', State.screen_top1.line)
   Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
---?   print('pagedown end')
+end
+
+-- return the location of the start of the bottom-most line on screen
+function Text.screen_bottom1(State)
+  -- duplicate some logic from love.draw
+  -- does not modify State (except to populate line_cache)
+  local loc2 = Text.to2(State, State.screen_top1)
+  local y = State.top
+  while true do
+    y = y + State.line_height
+    if y + State.line_height > App.screen.height then break end
+    local next_loc2 = Text.next_screen_line(State, loc2)
+    if Text.eq2(next_loc2, loc2) then break end
+    loc2 = next_loc2
+  end
+  return Text.to1(State, loc2)
 end
 
 function Text.up(State)
@@ -306,7 +322,7 @@ function Text.up(State)
 end
 
 function Text.down(State)
---?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   assert(State.cursor1.pos, 'cursor has no pos')
   if Text.cursor_at_final_screen_line(State) then
     -- line is done, skip to next text line
@@ -317,7 +333,9 @@ function Text.down(State)
       State.cursor1.pos = Text.nearest_cursor_pos(State.font, State.lines[State.cursor1.line].data, State.cursor_x, State.left)
 --?       print(State.cursor1.pos)
     end
-    if State.cursor1.line > State.screen_bottom1.line then
+    local screen_bottom1 = Text.screen_bottom1(State)
+--?   print('down 2', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, screen_bottom1.line, screen_bottom1.pos)
+    if State.cursor1.line > screen_bottom1.line then
 --?       print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
 --?       print('scroll up preserving cursor')
       Text.snap_cursor_to_bottom_of_screen(State)
@@ -325,7 +343,8 @@ function Text.down(State)
     end
   else
     -- move down one screen line in current line
-    local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
+    local screen_bottom1 = Text.screen_bottom1(State)
+    local scroll_down = Text.le1(screen_bottom1, State.cursor1)
 --?     print('cursor is NOT at final screen line of its line')
     local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
     Text.populate_screen_line_starting_pos(State, State.cursor1.line)
@@ -341,7 +360,7 @@ function Text.down(State)
 --?       print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
     end
   end
---?   print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
 end
 
 function Text.start_of_line(State)
@@ -474,6 +493,14 @@ function Text.pos_at_end_of_screen_line(State, loc1)
   assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
+function Text.final_text_loc_on_screen(State)
+  local screen_bottom1 = Text.screen_bottom1(State)
+  return {
+    line=screen_bottom1.line,
+    pos=Text.pos_at_end_of_screen_line(State, screen_bottom1),
+  }
+end
+
 function Text.cursor_at_final_screen_line(State)
   Text.populate_screen_line_starting_pos(State, State.cursor1.line)
   local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos
@@ -495,7 +522,7 @@ function Text.snap_cursor_to_bottom_of_screen(State)
 --?   print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
   -- slide to start of screen line
   top2.screen_pos = 1  -- start of screen line
---?   print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
 --?   print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
   local y = App.screen.height - State.line_height
   -- duplicate some logic from love.draw
@@ -512,26 +539,28 @@ function Text.snap_cursor_to_bottom_of_screen(State)
 --?   print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
   State.screen_top1 = Text.to1(State, top2)
 --?   print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
---?   print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
+--?   print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
   Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
 end
 
 function Text.in_line(State, line_index, x,y)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  if line_cache.starty == nil then return false end  -- outside current page
-  if y < line_cache.starty then return false end
+  local starty = Text.starty(State, line_index)
+  if starty == nil then return false end  -- outside current page
+  if y < starty then return false end
   Text.populate_screen_line_starting_pos(State, line_index)
-  return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
+  return y < starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
 end
 
 -- convert mx,my in pixels to schema-1 coordinates
 function Text.to_pos_on_line(State, line_index, mx, my)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  assert(my >= line_cache.starty, 'failed to map y pixel to line')
+  local starty = Text.starty(State, line_index)
+  assert(my >= starty, 'failed to map y pixel to line')
   -- duplicate some logic from Text.draw
-  local y = line_cache.starty
+  local y = starty
   local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
   for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
     local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
@@ -714,6 +743,10 @@ function Text.le1(a, b)
   return a.pos <= b.pos
 end
 
+function Text.eq2(a, b)
+  return a.line == b.line and a.screen_line == b.screen_line and a.screen_pos == b.screen_pos
+end
+
 function Text.offset(s, pos1)
   if pos1 == 1 then return 1 end
   local result = utf8.offset(s, pos1)
@@ -735,6 +768,19 @@ function Text.previous_screen_line(State, loc2)
   end
 end
 
+function Text.next_screen_line(State, loc2)
+  Text.populate_screen_line_starting_pos(State, loc2.line)
+  if loc2.screen_line >= #State.line_cache[loc2.line].screen_line_starting_pos then
+    if loc2.line < #State.lines then
+      return {line=loc2.line+1, screen_line=1, screen_pos=1}
+    else
+      return loc2
+    end
+  else
+    return {line=loc2.line, screen_line=loc2.screen_line+1, screen_pos=1}
+  end
+end
+
 -- resize helper
 function Text.tweak_screen_top_and_cursor(State)
   if State.screen_top1.pos == 1 then return end
@@ -758,16 +804,12 @@ function Text.tweak_screen_top_and_cursor(State)
     end
   end
   -- make sure cursor is on screen
+  local screen_bottom1 = Text.screen_bottom1(State)
   if Text.lt1(State.cursor1, State.screen_top1) then
     State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-  elseif State.cursor1.line >= State.screen_bottom1.line then
---?     print('too low')
+  elseif State.cursor1.line >= screen_bottom1.line then
     if Text.cursor_out_of_screen(State) then
---?       print('tweak')
-      State.cursor1 = {
-          line=State.screen_bottom1.line,
-          pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),
-      }
+      State.cursor1 = Text.final_text_loc_on_screen(State)
     end
   end
 end
@@ -776,11 +818,6 @@ end
 function Text.cursor_out_of_screen(State)
   edit.draw(State)
   return State.cursor_y == nil
-  -- this approach is cheaper and almost works, except on the final screen
-  -- where file ends above bottom of screen
---?   local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
---?   local botline1 = {line=State.cursor1.line, pos=botpos}
---?   return Text.lt1(State.screen_bottom1, botline1)
 end
 
 function Text.redraw_all(State)
diff --git a/text_tests.lua b/text_tests.lua
index 541a056..ca97e7f 100644
--- a/text_tests.lua
+++ b/text_tests.lua
@@ -191,9 +191,8 @@ function test_click_moves_cursor()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
@@ -210,7 +209,6 @@ function test_click_to_left_of_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=3}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click to the left of the line
   edit.draw(Editor_state)
@@ -230,7 +228,6 @@ function test_click_takes_margins_into_account()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click on the other line
   edit.draw(Editor_state)
@@ -249,7 +246,6 @@ function test_click_on_empty_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click on the empty line
   edit.draw(Editor_state)
@@ -268,7 +264,6 @@ function test_click_below_all_lines()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
   -- click below first line
   edit.draw(Editor_state)
@@ -286,7 +281,6 @@ function test_draw_text()
   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', 'screen:1')
@@ -303,7 +297,6 @@ function test_draw_wrapping_text()
   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', 'screen:1')
@@ -320,7 +313,6 @@ function test_draw_word_wrapping_text()
   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 ', 'screen:1')
@@ -338,7 +330,6 @@ function test_click_on_wrapping_line()
   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)
@@ -357,7 +348,6 @@ function test_click_on_wrapping_line_takes_margins_into_account()
   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)
@@ -375,7 +365,6 @@ function test_draw_text_wrapping_within_word()
   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 ', 'screen:1')
@@ -393,7 +382,6 @@ function test_draw_wrapping_text_containing_non_ascii()
   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', 'screen:1')
@@ -412,7 +400,6 @@ function test_click_past_end_of_screen_line()
   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 ', 'baseline/screen:1')
@@ -435,7 +422,6 @@ function test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()
   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", 'baseline/screen:2')
@@ -456,7 +442,6 @@ function test_click_past_end_of_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -480,7 +465,6 @@ function test_click_past_end_of_wrapping_line_containing_non_ascii()
   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 ', 'baseline/screen:1')
@@ -505,7 +489,6 @@ function test_click_past_end_of_word_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -524,7 +507,6 @@ function test_select_text()
   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)
   -- select a letter
   App.fake_key_press('lshift')
@@ -547,7 +529,6 @@ function test_cursor_movement_without_shift_resets_selection()
   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', 'right')
@@ -565,7 +546,6 @@ function test_copy_does_not_reset_selection()
   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', 'c')
@@ -575,15 +555,15 @@ function test_copy_does_not_reset_selection()
 end
 
 function test_move_cursor_using_mouse()
+
   App.screen.init{width=50, height=60}
   Editor_state = edit.initialize_test_state()
   Editor_state.lines = load_array{'abc', 'def', 'xyz'}
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
@@ -598,9 +578,8 @@ function test_select_text_using_mouse()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- press and hold on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   -- drag and release somewhere else
@@ -618,9 +597,8 @@ function test_select_text_using_mouse_starting_above_text()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- press mouse above first line of text
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
   check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
@@ -636,7 +614,6 @@ function test_select_text_using_mouse_starting_above_text_wrapping_line()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=5}
   Editor_state.screen_top1 = {line=2, pos=3}
-  Editor_state.screen_bottom1 = {}
   -- press mouse above first line of text
   edit.draw(Editor_state)
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
@@ -659,7 +636,6 @@ function test_select_text_using_mouse_starting_below_text()
   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, 'ab', 'baseline:screen:1')
@@ -680,9 +656,8 @@ function test_select_text_using_mouse_and_shift()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- click on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
@@ -705,9 +680,8 @@ function test_select_text_repeatedly_using_mouse_and_shift()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   Editor_state.selection1 = {}
-  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
   -- click on first location
   edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
@@ -735,7 +709,6 @@ function test_select_all_text()
   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)
   -- select all
   App.fake_key_press('lctrl')
@@ -756,7 +729,6 @@ function test_pagedown()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- initially the first two lines are displayed
   edit.draw(Editor_state)
   local y = Editor_state.top
@@ -773,36 +745,6 @@ function test_pagedown()
   App.screen.check(y, 'ghi', 'screen:2')
 end
 
-function test_pagedown_often_shows_start_of_wrapping_line()
-  -- draw a few lines ending in part of a wrapping line
-  App.screen.init{width=50, 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=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', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def ', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi ', 'baseline/screen:3')
-  -- after pagedown we start drawing from the bottom _line_ (multiple screen lines)
-  edit.run_after_keychord(Editor_state, 'pagedown', 'pagedown')
-  check_eq(Editor_state.screen_top1.line, 2, 'screen_top:line')
-  check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'def ', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi ', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'jkl', 'screen:3')
-end
-
 function test_pagedown_can_start_from_middle_of_long_wrapping_line()
   -- draw a few lines starting from a very long wrapping line
   App.screen.init{width=Editor_state.left+30, height=60}
@@ -811,7 +753,6 @@ function test_pagedown_can_start_from_middle_of_long_wrapping_line()
   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 ', 'baseline/screen:1')
@@ -846,7 +787,6 @@ function test_pagedown_never_moves_up()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=9}
   Editor_state.screen_top1 = {line=1, pos=9}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- pagedown makes no change
   edit.run_after_keychord(Editor_state, 'pagedown', 'pagedown')
@@ -861,7 +801,6 @@ function test_down_arrow_moves_cursor()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   -- initially the first three lines are displayed
   edit.draw(Editor_state)
   local y = Editor_state.top
@@ -891,7 +830,6 @@ function test_down_arrow_scrolls_down_by_one_line()
   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', 'baseline/screen:1')
@@ -919,7 +857,6 @@ function test_down_arrow_scrolls_down_by_one_screen_line()
   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', 'baseline/screen:1')
@@ -948,7 +885,6 @@ function test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_
   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', 'baseline/screen:1')
@@ -976,7 +912,6 @@ function test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up()
   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', 'baseline/screen:1')
@@ -1010,7 +945,6 @@ function test_up_arrow_moves_cursor()
   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', 'baseline/screen:1')
@@ -1039,7 +973,6 @@ function test_up_arrow_scrolls_up_by_one_line()
   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', 'baseline/screen:1')
@@ -1067,7 +1000,6 @@ function test_up_arrow_scrolls_up_by_one_screen_line()
   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', 'baseline/screen:1')
@@ -1095,7 +1027,6 @@ function test_up_arrow_scrolls_up_to_final_screen_line()
   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', 'baseline/screen:1')
@@ -1125,7 +1056,6 @@ function test_up_arrow_scrolls_up_to_empty_line()
   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', 'baseline/screen:1')
@@ -1152,7 +1082,6 @@ function test_pageup()
   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
@@ -1177,7 +1106,6 @@ function test_pageup_scrolls_up_by_screen_line()
   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', 'baseline/screen:1')
@@ -1206,7 +1134,6 @@ function test_pageup_scrolls_up_from_middle_screen_line()
   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', 'baseline/screen:2')
@@ -1232,7 +1159,6 @@ function test_left_arrow_scrolls_up_in_wrapped_line()
   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)
@@ -1261,7 +1187,6 @@ function test_right_arrow_scrolls_down_in_wrapped_line()
   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)
@@ -1291,7 +1216,6 @@ function test_home_scrolls_up_in_wrapped_line()
   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)
@@ -1320,7 +1244,6 @@ function test_end_scrolls_down_in_wrapped_line()
   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)
@@ -1350,7 +1273,6 @@ function test_search()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -1377,7 +1299,6 @@ function test_search_upwards()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -1395,7 +1316,6 @@ function test_search_wrap()
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=2, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.screen_bottom1 = {}
   edit.draw(Editor_state)
   -- search for a string
   edit.run_after_keychord(Editor_state, 'C-f', 'f')
@@ -1413,7 +1333,6 @@ function test_search_wrap_upwards()
   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', 'f')
>21bd828 ^
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755