about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2023-03-17 22:29:41 -0700
committerKartik K. Agaram <vc@akkartik.com>2023-03-17 22:30:46 -0700
commiteca407cf00072a61660045cf08def0350058f572 (patch)
treea8d3f4ae2489e04aed3eba3b6d721642f6b43abd
parent4dbc097f835218bc44dd1d96134294a0bc5ce725 (diff)
parent6709b394fba8dbc8cdfaaf3a9461e79ce7b0c2e0 (diff)
downloadtext.love-eca407cf00072a61660045cf08def0350058f572.tar.gz
Merge lines.love
-rw-r--r--edit.lua11
-rw-r--r--log_browser.lua6
-rw-r--r--source.lua14
-rw-r--r--source_edit.lua117
-rw-r--r--source_text.lua923
-rw-r--r--source_text_tests.lua22
-rw-r--r--text.lua26
7 files changed, 210 insertions, 909 deletions
diff --git a/edit.lua b/edit.lua
index 865682d..722eddb 100644
--- a/edit.lua
+++ b/edit.lua
@@ -92,7 +92,10 @@ end
 
 function edit.draw(State)
   App.color(Text_color)
-  assert(#State.lines == #State.line_cache)
+  if #State.lines ~= #State.line_cache then
+    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
+    assert(false)
+  end
   if not Text.le1(State.screen_top1, State.cursor1) then
     print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
     assert(false)
@@ -254,8 +257,10 @@ function edit.keychord_press(State, chord, key)
     edit.update_font_settings(State, State.font_height+2)
     Text.redraw_all(State)
   elseif chord == 'C--' then
-    edit.update_font_settings(State, State.font_height-2)
-    Text.redraw_all(State)
+    if State.font_height > 2 then
+      edit.update_font_settings(State, State.font_height-2)
+      Text.redraw_all(State)
+    end
   elseif chord == 'C-0' then
     edit.update_font_settings(State, 20)
     Text.redraw_all(State)
diff --git a/log_browser.lua b/log_browser.lua
index 76596f2..5946e3e 100644
--- a/log_browser.lua
+++ b/log_browser.lua
@@ -12,7 +12,7 @@ function source.initialize_log_browser_side()
   log_browser.parse(Log_browser_state)
   Text.redraw_all(Log_browser_state)
   Log_browser_state.screen_top1 = {line=1, pos=1}
-  Log_browser_state.cursor1 = {line=1, pos=nil}
+  Log_browser_state.cursor1 = {line=1, pos=1}
 end
 
 Section_stack = {}
@@ -237,14 +237,12 @@ function log_browser.mouse_press(State, x,y, mouse_button)
     source.switch_to_file(line.filename)
   end
   -- set cursor
-  Editor_state.cursor1 = {line=line.line_number, pos=1, posB=nil}
+  Editor_state.cursor1 = {line=line.line_number, pos=1}
   -- make sure it's visible
   -- TODO: handle extremely long lines
   Editor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)
   -- show cursor
   Focus = 'edit'
-  -- expand B side
-  Editor_state.expanded = true
 end
 
 function log_browser.line_index(State, mx,my)
diff --git a/source.lua b/source.lua
index 3ac51d5..da168f4 100644
--- a/source.lua
+++ b/source.lua
@@ -75,7 +75,7 @@ function source.initialize()
   love.window.setTitle('text.love - source')
 end
 
--- environment for a mutable file of bifolded text
+-- environment for a mutable file
 -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
 function source.initialize_edit_side()
   load_from_disk(Editor_state)
@@ -89,17 +89,12 @@ function source.initialize_edit_side()
   end
   edit.check_locs(Editor_state)
 
-  -- We currently start out with side B collapsed.
-  -- Other options:
-  --  * save all expanded state by line
-  --  * expand all if any location is in side B
   if Editor_state.cursor1.line > #Editor_state.lines then
     Editor_state.cursor1 = {line=1, pos=1}
   end
   if Editor_state.screen_top1.line > #Editor_state.lines then
     Editor_state.screen_top1 = {line=1, pos=1}
   end
-  edit.eradicate_locations_after_the_fold(Editor_state)
 
   if rawget(_G, 'jit') then
     jit.off()
@@ -253,13 +248,6 @@ end
 function source.quit()
   edit.quit(Editor_state)
   log_browser.quit(Log_browser_state)
-  -- convert any bifold files here
-end
-
-function source.convert_bifold_text(infilename, outfilename)
-  local contents = love.filesystem.read(infilename)
-  contents = contents:gsub('\u{1e}', ';')
-  love.filesystem.write(outfilename, contents)
 end
 
 function source.settings()
diff --git a/source_edit.lua b/source_edit.lua
index f340ab3..964f6ff 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -10,8 +10,6 @@ Highlight_color = {r=0.7, g=0.7, b=0.9}  -- selected text
 Icon_color = {r=0.7, g=0.7, b=0.7}  -- color of current mode icon in drawings
 Help_color = {r=0, g=0.5, b=0}
 Help_background_color = {r=0, g=0.5, b=0, a=0.1}
-Fold_color = {r=0, g=0.6, b=0}
-Fold_background_color = {r=0, g=0.7, b=0}
 
 Margin_top = 15
 Margin_left = 25
@@ -28,12 +26,10 @@ edit = {}
 -- run in both tests and a real run
 function edit.initialize_state(top, left, right, font_height, line_height)  -- currently always draws to bottom of screen
   local result = {
-    -- a line is either bifold text or a drawing
-    -- a line of bifold text consists of an A side and an optional B side
+    -- a line is either text or a drawing
+    -- a text is a table with:
     --    mode = 'text',
     --    string data,
-    --    string dataB,
-    --    expanded: whether to show B side
     -- a drawing is a table with:
     --    mode = 'drawing'
     --    a (y) coord in pixels (updated while painting screen),
@@ -50,7 +46,7 @@ function edit.initialize_state(top, left, right, font_height, line_height)  -- c
     -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide
     -- The field names are carefully chosen so that switching modes in midstream
     -- remembers previously entered points where that makes sense.
-    lines = {{mode='text', data='', dataB=nil, expanded=nil}},  -- array of lines
+    lines = {{mode='text', data=''}},  -- array of lines
 
     -- Lines can be too long to fit on screen, in which case they _wrap_ into
     -- multiple _screen lines_.
@@ -65,16 +61,15 @@ function edit.initialize_state(top, left, right, font_height, line_height)  -- c
     -- Given wrapping, any potential location for the text cursor can be described in two ways:
     -- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
     -- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
-    -- Positions (and screen line indexes) can be in either the A or the B side.
     --
     -- Most of the time we'll only persist positions in schema 1, translating to
     -- schema 2 when that's convenient.
     --
     -- Make sure these coordinates are never aliased, so that changing one causes
     -- action at a distance.
-    screen_top1 = {line=1, pos=1, posB=nil},  -- position of start of screen line at top of screen
-    cursor1 = {line=1, pos=1, posB=nil},  -- position of cursor
-    screen_bottom1 = {line=1, pos=1, posB=nil},  -- position of start of screen line at bottom of screen
+    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
 
     selection1 = {},
     -- some extra state to compute selection between mouse press and release
@@ -96,8 +91,8 @@ function edit.initialize_state(top, left, right, font_height, line_height)  -- c
     em = App.newText(love.graphics.getFont(), 'm'),  -- widest possible character width
 
     top = top,
-    left = left,
-    right = right,
+    left = math.floor(left),
+    right = math.floor(right),
     width = right-left,
 
     filename = love.filesystem.getUserDirectory()..'/lines.txt',  -- '/' should work even on Windows
@@ -154,7 +149,7 @@ function edit.draw(State, hide_cursor)
     assert(false)
   end
   if not Text.le1(State.screen_top1, State.cursor1) then
-    print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
+    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
     assert(false)
   end
   State.cursor_x = nil
@@ -163,18 +158,14 @@ function edit.draw(State, hide_cursor)
 --?   print('== draw')
   for line_index = State.screen_top1.line,#State.lines do
     local line = State.lines[line_index]
---?     print('draw:', y, line_index, line, line.mode)
+--?     print('draw:', y, line_index, line)
     if y + State.line_height > App.screen.height then break end
-    State.screen_bottom1 = {line=line_index, pos=nil, posB=nil}
+    State.screen_bottom1 = {line=line_index, pos=nil}
     if line.mode == 'text' then
---?     print('text.draw', y, line_index)
-      local startpos, startposB = 1, nil
+--?       print('text.draw', y, line_index)
+      local startpos = 1
       if line_index == State.screen_top1.line then
-        if State.screen_top1.pos then
-          startpos = State.screen_top1.pos
-        else
-          startpos, startposB = nil, State.screen_top1.posB
-        end
+        startpos = State.screen_top1.pos
       end
       if line.data == '' then
         -- button to insert new drawing
@@ -192,7 +183,7 @@ function edit.draw(State, hide_cursor)
                      end,
         })
       end
-      y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB, hide_cursor)
+      y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos, hide_cursor)
       y = y + State.line_height
 --?       print('=> y', y)
     elseif line.mode == 'drawing' then
@@ -227,12 +218,14 @@ function edit.quit(State)
   -- make sure to save before quitting
   if State.next_save then
     save_to_disk(State)
+    -- give some time for the OS to flush everything to disk
+    love.timer.sleep(0.1)
   end
 end
 
 function edit.mouse_press(State, x,y, mouse_button)
   if State.search_term then return end
---?   print('press')
+--?   print('press', State.selection1.line, State.selection1.pos)
   if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
     -- press on a button and it returned 'true' to short-circuit
     return
@@ -253,10 +246,11 @@ 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()
-        local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
-  --?       print(x,y, 'setting cursor:', line_index, pos, posB)
-        State.selection1 = {line=line_index, pos=pos, posB=posB}
---?         print('selection', State.selection1.line, State.selection1.pos, State.selection1.posB)
+        State.selection1 = {
+            line=line_index,
+            pos=Text.to_pos_on_line(State, line_index, x, y),
+        }
+--?         print('selection', State.selection1.line, State.selection1.pos)
         break
       end
     elseif line.mode == 'drawing' then
@@ -287,9 +281,11 @@ function edit.mouse_release(State, x,y, mouse_button)
       if line.mode == 'text' then
         if Text.in_line(State, line_index, x,y) then
 --?           print('reset selection')
-          local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
-          State.cursor1 = {line=line_index, pos=pos, posB=posB}
---?           print('cursor', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
+          State.cursor1 = {
+              line=line_index,
+              pos=Text.to_pos_on_line(State, line_index, x, y),
+          }
+--?           print('cursor', State.cursor1.line, State.cursor1.pos)
           if State.mousepress_shift then
             if State.old_selection1.line == nil then
               State.selection1 = State.old_cursor1
@@ -358,11 +354,7 @@ function edit.keychord_press(State, chord, key)
       State.search_term = string.sub(State.search_term, 1, byte_offset-1)
       State.search_text = nil
     elseif chord == 'down' then
-      if State.cursor1.pos then
-        State.cursor1.pos = State.cursor1.pos+1
-      else
-        State.cursor1.posB = State.cursor1.posB+1
-      end
+      State.cursor1.pos = State.cursor1.pos+1
       Text.search_next(State)
     elseif chord == 'up' then
       Text.search_previous(State)
@@ -371,42 +363,19 @@ function edit.keychord_press(State, chord, key)
   elseif chord == 'C-f' then
     State.search_term = ''
     State.search_backup = {
-      cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},
-      screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},
+      cursor={line=State.cursor1.line, pos=State.cursor1.pos},
+      screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
     }
     assert(State.search_text == nil)
-  -- bifold text
-  elseif chord == 'M-b' then
-    State.expanded = not State.expanded
-    Text.redraw_all(State)
-    if not State.expanded then
-      for _,line in ipairs(State.lines) do
-        line.expanded = nil
-      end
-      edit.eradicate_locations_after_the_fold(State)
-    end
-  elseif chord == 'M-d' then
-    if State.cursor1.posB == nil then
-      local before = snapshot(State, State.cursor1.line)
-      if State.lines[State.cursor1.line].dataB == nil then
-        State.lines[State.cursor1.line].dataB = ''
-      end
-      State.lines[State.cursor1.line].expanded = true
-      State.cursor1.pos = nil
-      State.cursor1.posB = 1
-      if Text.cursor_out_of_screen(State) then
-        Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
-      end
-      schedule_save(State)
-      record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-    end
   -- zoom
   elseif chord == 'C-=' then
     edit.update_font_settings(State, State.font_height+2)
     Text.redraw_all(State)
   elseif chord == 'C--' then
-    edit.update_font_settings(State, State.font_height-2)
-    Text.redraw_all(State)
+    if State.font_height > 2 then
+      edit.update_font_settings(State, State.font_height-2)
+      Text.redraw_all(State)
+    end
   elseif chord == 'C-0' then
     edit.update_font_settings(State, 20)
     Text.redraw_all(State)
@@ -445,7 +414,7 @@ function edit.keychord_press(State, chord, key)
   -- clipboard
   elseif chord == 'C-a' then
     State.selection1 = {line=1, pos=1}
-    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1, posB=nil}
+    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
   elseif chord == 'C-c' then
     local s = Text.selection(State)
     if s then
@@ -520,20 +489,6 @@ function edit.keychord_press(State, chord, key)
   end
 end
 
-function edit.eradicate_locations_after_the_fold(State)
-  -- eradicate side B from any locations we track
-  if State.cursor1.posB then
-    State.cursor1.posB = nil
-    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
-    State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
-  end
-  if State.screen_top1.posB then
-    State.screen_top1.posB = nil
-    State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
-    State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
-  end
-end
-
 function edit.key_release(State, key, scancode)
 end
 
diff --git a/source_text.lua b/source_text.lua
index 7c00191..fb4c389 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -1,90 +1,16 @@
 -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
 Text = {}
-AB_padding = 20  -- space in pixels between A side and B side
 
 -- draw a line starting from startpos to screen at y between State.left and State.right
--- return the final y, and pos,posB of start of final screen line drawn
-function Text.draw(State, line_index, y, startpos, startposB, hide_cursor)
+-- return the final y, and position of start of final screen line drawn
+function Text.draw(State, line_index, y, startpos, hide_cursor)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
   line_cache.starty = y
   line_cache.startpos = startpos
-  line_cache.startposB = startposB
-  -- draw A side
-  local overflows_screen, x, pos, screen_line_starting_pos
-  if startpos then
-    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)
-    if overflows_screen then
-      return y, screen_line_starting_pos
-    end
-    if Focus == 'edit' and State.cursor1.pos then
-      if not hide_cursor and not State.search_term then
-        if line_index == State.cursor1.line and State.cursor1.pos == pos then
-          Text.draw_cursor(State, x, y)
-        end
-      end
-    end
-  else
-    x = State.left
-  end
-  -- check for B side
---?   if line_index == 8 then print('checking for B side') end
-  if line.dataB == nil then
-    assert(y)
-    assert(screen_line_starting_pos)
---?     if line_index == 8 then print('return 1') end
-    return y, screen_line_starting_pos
-  end
-  if not State.expanded and not line.expanded then
-    assert(y)
-    assert(screen_line_starting_pos)
---?     if line_index == 8 then print('return 2') end
-    button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},
-      icon = function(button_params)
-               App.color(Fold_background_color)
-               love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)
-             end,
-      onpress1 = function()
-                   line.expanded = true
-                 end,
-    })
-    return y, screen_line_starting_pos
-  end
-  -- draw B side
---?   if line_index == 8 then print('drawing B side') end
-  App.color(Fold_color)
-  if startposB then
-    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)
-  else
-    overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)
-  end
-  if overflows_screen then
-    return y, nil, screen_line_starting_pos
-  end
---?   if line_index == 8 then print('a') end
-  if Focus == 'edit' and State.cursor1.posB then
---?     if line_index == 8 then print('b') end
-    if not hide_cursor and not State.search_term then
---?       if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
-      if line_index == State.cursor1.line and State.cursor1.posB == pos then
-        Text.draw_cursor(State, x, y)
-      end
-    end
-  end
-  return y, nil, screen_line_starting_pos
-end
-
--- Given an array of fragments, draw the subset starting from pos to screen
--- starting from (x,y).
--- Return:
---  - whether we got to bottom of screen before end of line
---  - the final (x,y)
---  - the final pos
---  - starting pos of the final screen line drawn
-function Text.draw_wrapping_line(State, line_index, x,y, startpos)
-  local line = State.lines[line_index]
-  local line_cache = State.line_cache[line_index]
---?   print('== line', line_index, '^'..line.data..'$')
+  -- wrap long lines
+  local x = State.left
+  local pos = 1
   local screen_line_starting_pos = startpos
   Text.compute_fragments(State, line_index)
   local pos = 1
@@ -105,7 +31,7 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos)
         assert(x > State.left)  -- no overfull lines
         y = y + State.line_height
         if y + State.line_height > App.screen.height then
-          return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
+          return y, screen_line_starting_pos
         end
         screen_line_starting_pos = pos
         x = State.left
@@ -130,7 +56,7 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos)
       end
       App.screen.draw(frag_text, x,y)
       -- render cursor if necessary
-      if State.cursor1.pos and line_index == State.cursor1.line then
+      if line_index == State.cursor1.line then
         if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
           if State.search_term then
             if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
@@ -148,59 +74,12 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos)
     end
     pos = pos + frag_len
   end
-  return false, x,y, pos, screen_line_starting_pos
-end
-
-function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
-  local line = State.lines[line_index]
-  local line_cache = State.line_cache[line_index]
-  local screen_line_starting_pos = startpos
-  Text.compute_fragmentsB(State, line_index, x)
-  local pos = 1
-  for _, f in ipairs(line_cache.fragmentsB) do
-    local frag, frag_text = f.data, f.text
-    local frag_len = utf8.len(frag)
---?     print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
-    if pos < startpos then
-      -- render nothing
---?       print('skipping', frag)
-    else
-      -- render fragment
-      local frag_width = App.width(frag_text)
-      if x + frag_width > State.right then
-        assert(x > State.left)  -- no overfull lines
-        y = y + State.line_height
-        if y + State.line_height > App.screen.height then
-          return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
-        end
-        screen_line_starting_pos = pos
-        x = State.left
-      end
-      if State.selection1.line then
-        local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
-        Text.draw_highlight(State, line, x,y, pos, lo,hi)
-      end
-      App.screen.draw(frag_text, x,y)
-      -- render cursor if necessary
-      if State.cursor1.posB and line_index == State.cursor1.line then
-        if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then
-          if State.search_term then
-            if State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term then
-              local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term))
-              App.color(Fold_color)
-              love.graphics.print(State.search_term, x+lo_px,y)
-            end
-          elseif Focus == 'edit' then
-            Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)
-            App.color(Fold_color)
-          end
-        end
-      end
-      x = x + frag_width
+  if Focus == 'edit' and not hide_cursor and State.search_term == nil then
+    if line_index == State.cursor1.line and State.cursor1.pos == pos then
+      Text.draw_cursor(State, x, y)
     end
-    pos = pos + frag_len
   end
-  return false, x,y, pos, screen_line_starting_pos
+  return y, screen_line_starting_pos
 end
 
 function Text.draw_cursor(State, x, y)
@@ -285,74 +164,6 @@ function Text.compute_fragments(State, line_index)
   end
 end
 
-function Text.populate_screen_line_starting_posB(State, line_index, x)
-  local line = State.lines[line_index]
-  local line_cache = State.line_cache[line_index]
-  if line_cache.screen_line_starting_posB then
-    return
-  end
-  -- duplicate some logic from Text.draw
-  Text.compute_fragmentsB(State, line_index, x)
-  line_cache.screen_line_starting_posB = {1}
-  local pos = 1
-  for _, f in ipairs(line_cache.fragmentsB) do
-    local frag, frag_text = f.data, f.text
-    -- render fragment
-    local frag_width = App.width(frag_text)
-    if x + frag_width > State.right then
-      x = State.left
-      table.insert(line_cache.screen_line_starting_posB, pos)
-    end
-    x = x + frag_width
-    local frag_len = utf8.len(frag)
-    pos = pos + frag_len
-  end
-end
-
-function Text.compute_fragmentsB(State, line_index, x)
---?   print('compute_fragmentsB', line_index, 'between', x, State.right)
-  local line = State.lines[line_index]
-  local line_cache = State.line_cache[line_index]
-  if line_cache.fragmentsB then
-    return
-  end
-  line_cache.fragmentsB = {}
-  -- try to wrap at word boundaries
-  for frag in line.dataB:gmatch('%S*%s*') do
-    local frag_text = App.newText(love.graphics.getFont(), frag)
-    local frag_width = App.width(frag_text)
---?     print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
-    while x + frag_width > State.right do
---?       print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
-      if (x-State.left) < 0.8 * (State.right-State.left) then
---?         print('splitting')
-        -- long word; chop it at some letter
-        -- We're not going to reimplement TeX here.
-        local bpos = Text.nearest_pos_less_than(frag, State.right - x)
---?         print('bpos', bpos)
-        if bpos == 0 then break end  -- avoid infinite loop when window is too narrow
-        local boffset = Text.offset(frag, bpos+1)  -- byte _after_ bpos
---?         print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
-        local frag1 = string.sub(frag, 1, boffset-1)
-        local frag1_text = App.newText(love.graphics.getFont(), frag1)
-        local frag1_width = App.width(frag1_text)
---?         print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
-        assert(x + frag1_width <= State.right)
-        table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})
-        frag = string.sub(frag, boffset)
-        frag_text = App.newText(love.graphics.getFont(), frag)
-        frag_width = App.width(frag_text)
-      end
-      x = State.left  -- new line
-    end
-    if #frag > 0 then
---?       print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
-      table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})
-    end
-    x = x + frag_width
-  end
-end
-
 function Text.text_input(State, t)
   if App.mouse_down(1) then return end
   if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end
@@ -367,18 +178,10 @@ function Text.text_input(State, t)
 end
 
 function Text.insert_at_cursor(State, t)
-  if State.cursor1.pos then
-    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
-    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
-    Text.clear_screen_line_cache(State, State.cursor1.line)
-    State.cursor1.pos = State.cursor1.pos+1
-  else
-    assert(State.cursor1.posB)
-    local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
-    State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)
-    Text.clear_screen_line_cache(State, State.cursor1.line)
-    State.cursor1.posB = State.cursor1.posB+1
-  end
+  local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
+  State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
+  Text.clear_screen_line_cache(State, State.cursor1.line)
+  State.cursor1.pos = State.cursor1.pos+1
 end
 
 -- Don't handle any keys here that would trigger text_input above.
@@ -413,7 +216,7 @@ function Text.keychord_press(State, chord)
       return
     end
     local before
-    if State.cursor1.pos and State.cursor1.pos > 1 then
+    if State.cursor1.pos > 1 then
       before = snapshot(State, State.cursor1.line)
       local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)
       local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
@@ -425,22 +228,6 @@ function Text.keychord_press(State, chord)
         end
         State.cursor1.pos = State.cursor1.pos-1
       end
-    elseif State.cursor1.posB then
-      if State.cursor1.posB > 1 then
-        before = snapshot(State, State.cursor1.line)
-        local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)
-        local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
-        if byte_start then
-          if byte_end then
-            State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
-          else
-            State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
-          end
-          State.cursor1.posB = State.cursor1.posB-1
-        end
-      else
-        -- refuse to delete past beginning of side B
-      end
     elseif State.cursor1.line > 1 then
       before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
       if State.lines[State.cursor1.line-1].mode == 'drawing' then
@@ -460,9 +247,10 @@ function Text.keychord_press(State, chord)
       local line_cache = State.line_cache[#State.line_cache]
       State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
     elseif Text.lt1(State.cursor1, State.screen_top1) then
-      local top2 = Text.to2(State, State.screen_top1)
-      top2 = Text.previous_screen_line(State, top2, State.left, State.right)
-      State.screen_top1 = Text.to1(State, top2)
+      State.screen_top1 = {
+        line=State.cursor1.line,
+        pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+      }
       Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
     end
     Text.clear_screen_line_cache(State, State.cursor1.line)
@@ -476,12 +264,12 @@ function Text.keychord_press(State, chord)
       return
     end
     local before
-    if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
+    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
       before = snapshot(State, State.cursor1.line)
     else
       before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
     end
-    if State.cursor1.pos and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
+    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
       local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
       local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
       if byte_start then
@@ -492,27 +280,10 @@ function Text.keychord_press(State, chord)
         end
         -- no change to State.cursor1.pos
       end
-    elseif State.cursor1.posB then
-      if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
-        local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
-        local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)
-        if byte_start then
-          if byte_end then
-            State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
-          else
-            State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
-          end
-          -- no change to State.cursor1.pos
-        end
-      else
-        -- refuse to delete past end of side B
-      end
     elseif State.cursor1.line < #State.lines then
       if State.lines[State.cursor1.line+1].mode == 'text' then
         -- join lines
         State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
-        -- delete side B on first line
-        State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
       end
       table.remove(State.lines, State.cursor1.line+1)
       table.remove(State.line_cache, State.cursor1.line+1)
@@ -529,12 +300,12 @@ function Text.keychord_press(State, chord)
     State.selection1 = {}
   elseif chord == 'S-left' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.left(State)
   elseif chord == 'S-right' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.right(State)
   -- C- hotkeys reserved for drawings, so we'll use M-
@@ -546,12 +317,12 @@ function Text.keychord_press(State, chord)
     State.selection1 = {}
   elseif chord == 'M-S-left' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.word_left(State)
   elseif chord == 'M-S-right' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.word_right(State)
   elseif chord == 'home' then
@@ -562,12 +333,12 @@ function Text.keychord_press(State, chord)
     State.selection1 = {}
   elseif chord == 'S-home' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.start_of_line(State)
   elseif chord == 'S-end' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.end_of_line(State)
   elseif chord == 'up' then
@@ -578,12 +349,12 @@ function Text.keychord_press(State, chord)
     State.selection1 = {}
   elseif chord == 'S-up' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.up(State)
   elseif chord == 'S-down' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.down(State)
   elseif chord == 'pageup' then
@@ -594,30 +365,24 @@ function Text.keychord_press(State, chord)
     State.selection1 = {}
   elseif chord == 'S-pageup' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.pageup(State)
   elseif chord == 'S-pagedown' then
     if State.selection1.line == nil then
-      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
     end
     Text.pagedown(State)
   end
 end
 
 function Text.insert_return(State)
-  if State.cursor1.pos then
-    -- when inserting a newline, move any B side to the new line
-    local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
-    table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
-    table.insert(State.line_cache, State.cursor1.line+1, {})
-    State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
-    State.lines[State.cursor1.line].dataB = nil
-    Text.clear_screen_line_cache(State, State.cursor1.line)
-    State.cursor1 = {line=State.cursor1.line+1, pos=1}
-  else
-    -- disable enter when cursor is on the B side
-  end
+  local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
+  table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
+  table.insert(State.line_cache, State.cursor1.line+1, {})
+  State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
+  Text.clear_screen_line_cache(State, State.cursor1.line)
+  State.cursor1 = {line=State.cursor1.line+1, pos=1}
 end
 
 function Text.pageup(State)
@@ -628,7 +393,7 @@ function Text.pageup(State)
   local y = App.screen.height - State.line_height
   while y >= State.top do
 --?     print(y, top2.line, top2.screen_line, top2.screen_pos)
-    if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break end
+    if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end
     if State.lines[State.screen_top1.line].mode == 'text' then
       y = y - State.line_height
     elseif State.lines[State.screen_top1.line].mode == 'drawing' then
@@ -637,7 +402,7 @@ function Text.pageup(State)
     top2 = Text.previous_screen_line(State, top2)
   end
   State.screen_top1 = Text.to1(State, top2)
-  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}
+  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')
@@ -645,15 +410,21 @@ 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, posB=State.screen_bottom1.posB}
+    State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
   end
 --?   print('setting top to', State.screen_top1.line, State.screen_top1.pos)
-  State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}
+  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
@@ -662,14 +433,6 @@ end
 
 function Text.up(State)
   assert(State.lines[State.cursor1.line].mode == 'text')
-  if State.cursor1.pos then
-    Text.upA(State)
-  else
-    Text.upB(State)
-  end
-end
-
-function Text.upA(State)
 --?   print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
   if screen_line_starting_pos == 1 then
@@ -703,68 +466,14 @@ function Text.upA(State)
 --?     print('cursor pos is now '..tostring(State.cursor1.pos))
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
-  end
-end
-
-function Text.upB(State)
-  local line_cache = State.line_cache[State.cursor1.line]
-  local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
-  assert(screen_line_indexB >= 1)
-  if screen_line_indexB == 1 then
-    -- move to A side of previous line
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line > 1 do
-      new_cursor_line = new_cursor_line-1
-      if State.lines[new_cursor_line].mode == 'text' then
-        State.cursor1 = {line=new_cursor_line, posB=nil}
-        Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-        local prev_line_cache = State.line_cache[State.cursor1.line]
-        local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
-        local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
-        local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
-        State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
-        break
-      end
-    end
-  elseif screen_line_indexB == 2 then
-    -- all-B screen-line to potentially A+B screen-line
-    local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
-    if State.cursor_x < xA then
-      State.cursor1.posB = nil
-      Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-      local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]
-      local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
-      local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
-      State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
-    else
-      Text.populate_screen_line_starting_posB(State, State.cursor1.line)
-      local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
-      local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
-      local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
-      State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1
-    end
-  else
-    assert(screen_line_indexB > 2)
-    -- all-B screen-line to all-B screen-line
-    Text.populate_screen_line_starting_posB(State, State.cursor1.line)
-    local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
-    local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
-    local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
-    State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
-  end
-  if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
+    State.screen_top1 = {
+      line=State.cursor1.line,
+      pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+    }
+    Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
   end
 end
 
--- cursor on final screen line (A or B side) => goes to next screen line on A side
--- cursor on A side => move down one screen line (A side) in current line
--- cursor on B side => move down one screen line (B side) in current line
 function Text.down(State)
   assert(State.lines[State.cursor1.line].mode == 'text')
 --?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
@@ -789,8 +498,8 @@ function Text.down(State)
       Text.snap_cursor_to_bottom_of_screen(State)
 --?       print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
     end
-  elseif State.cursor1.pos then
-    -- move down one screen line (A side) in current line
+  else
+    -- move down one screen line in current line
     local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
 --?     print('cursor is NOT at final screen line of its line')
     local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
@@ -806,85 +515,26 @@ function Text.down(State)
       Text.snap_cursor_to_bottom_of_screen(State)
 --?       print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
     end
-  else
-    -- move down one screen line (B side) in current line
-    local scroll_down = false
-    if Text.le1(State.screen_bottom1, State.cursor1) then
-      scroll_down = true
-    end
-    local cursor_line = State.lines[State.cursor1.line]
-    local cursor_line_cache = State.line_cache[State.cursor1.line]
-    local cursor2 = Text.to2(State, State.cursor1)
-    assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB)
-    local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
-    Text.populate_screen_line_starting_posB(State, State.cursor1.line)
-    local new_screen_line_starting_posB = cursor_line_cache.screen_line_starting_posB[screen_line_indexB+1]
-    local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB)
-    local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB)
-    State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
-    if scroll_down then
-      Text.snap_cursor_to_bottom_of_screen(State)
-    end
   end
 --?   print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
 end
 
 function Text.start_of_line(State)
-  if State.cursor1.pos then
-    State.cursor1.pos = 1
-  else
-    State.cursor1.posB = 1
-  end
+  State.cursor1.pos = 1
   if Text.lt1(State.cursor1, State.screen_top1) then
-    State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}  -- copy
+    State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos}  -- copy
   end
 end
 
 function Text.end_of_line(State)
-  if State.cursor1.pos then
-    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
-  else
-    State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1
-  end
+  State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
   if Text.cursor_out_of_screen(State) then
     Text.snap_cursor_to_bottom_of_screen(State)
   end
 end
 
 function Text.word_left(State)
-  -- we can cross the fold, so check side A/B one level down
-  Text.skip_whitespace_left(State)
-  Text.left(State)
-  Text.skip_non_whitespace_left(State)
-end
-
-function Text.word_right(State)
-  -- we can cross the fold, so check side A/B one level down
-  Text.skip_whitespace_right(State)
-  Text.right(State)
-  Text.skip_non_whitespace_right(State)
-  if Text.cursor_out_of_screen(State) then
-    Text.snap_cursor_to_bottom_of_screen(State)
-  end
-end
-
-function Text.skip_whitespace_left(State)
-  if State.cursor1.pos then
-    Text.skip_whitespace_leftA(State)
-  else
-    Text.skip_whitespace_leftB(State)
-  end
-end
-
-function Text.skip_non_whitespace_left(State)
-  if State.cursor1.pos then
-    Text.skip_non_whitespace_leftA(State)
-  else
-    Text.skip_non_whitespace_leftB(State)
-  end
-end
-
-function Text.skip_whitespace_leftA(State)
+  -- skip some whitespace
   while true do
     if State.cursor1.pos == 1 then
       break
@@ -894,22 +544,9 @@ function Text.skip_whitespace_leftA(State)
     end
     Text.left(State)
   end
-end
-
-function Text.skip_whitespace_leftB(State)
+  -- skip some non-whitespace
   while true do
-    if State.cursor1.posB == 1 then
-      break
-    end
-    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') then
-      break
-    end
     Text.left(State)
-  end
-end
-
-function Text.skip_non_whitespace_leftA(State)
-  while true do
     if State.cursor1.pos == 1 then
       break
     end
@@ -917,40 +554,11 @@ function Text.skip_non_whitespace_leftA(State)
     if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
       break
     end
-    Text.left(State)
   end
 end
 
-function Text.skip_non_whitespace_leftB(State)
-  while true do
-    if State.cursor1.posB == 1 then
-      break
-    end
-    assert(State.cursor1.posB > 1)
-    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then
-      break
-    end
-    Text.left(State)
-  end
-end
-
-function Text.skip_whitespace_right(State)
-  if State.cursor1.pos then
-    Text.skip_whitespace_rightA(State)
-  else
-    Text.skip_whitespace_rightB(State)
-  end
-end
-
-function Text.skip_non_whitespace_right(State)
-  if State.cursor1.pos then
-    Text.skip_non_whitespace_rightA(State)
-  else
-    Text.skip_non_whitespace_rightB(State)
-  end
-end
-
-function Text.skip_whitespace_rightA(State)
+function Text.word_right(State)
+  -- skip some whitespace
   while true do
     if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
       break
@@ -960,41 +568,17 @@ function Text.skip_whitespace_rightA(State)
     end
     Text.right_without_scroll(State)
   end
-end
-
-function Text.skip_whitespace_rightB(State)
   while true do
-    if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
-      break
-    end
-    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') then
-      break
-    end
     Text.right_without_scroll(State)
-  end
-end
-
-function Text.skip_non_whitespace_rightA(State)
-  while true do
     if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
       break
     end
     if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
       break
     end
-    Text.right_without_scroll(State)
   end
-end
-
-function Text.skip_non_whitespace_rightB(State)
-  while true do
-    if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
-      break
-    end
-    if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then
-      break
-    end
-    Text.right_without_scroll(State)
+  if Text.cursor_out_of_screen(State) then
+    Text.snap_cursor_to_bottom_of_screen(State)
   end
 end
 
@@ -1008,14 +592,7 @@ function Text.match(s, pos, pat)
 end
 
 function Text.left(State)
-  if State.cursor1.pos then
-    Text.leftA(State)
-  else
-    Text.leftB(State)
-  end
-end
-
-function Text.leftA(State)
+  assert(State.lines[State.cursor1.line].mode == 'text')
   if State.cursor1.pos > 1 then
     State.cursor1.pos = State.cursor1.pos-1
   else
@@ -1032,24 +609,11 @@ function Text.leftA(State)
     end
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
-  end
-end
-
-function Text.leftB(State)
-  if State.cursor1.posB > 1 then
-    State.cursor1.posB = State.cursor1.posB-1
-  else
-    -- overflow back into A side
-    State.cursor1.posB = nil
-    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
-  end
-  if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
+    State.screen_top1 = {
+      line=State.cursor1.line,
+      pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+    }
+    Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
   end
 end
 
@@ -1062,14 +626,6 @@ end
 
 function Text.right_without_scroll(State)
   assert(State.lines[State.cursor1.line].mode == 'text')
-  if State.cursor1.pos then
-    Text.right_without_scrollA(State)
-  else
-    Text.right_without_scrollB(State)
-  end
-end
-
-function Text.right_without_scrollA(State)
   if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
     State.cursor1.pos = State.cursor1.pos+1
   else
@@ -1084,22 +640,6 @@ function Text.right_without_scrollA(State)
   end
 end
 
-function Text.right_without_scrollB(State)
-  if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
-    State.cursor1.posB = State.cursor1.posB+1
-  else
-    -- overflow back into A side
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line <= #State.lines-1 do
-      new_cursor_line = new_cursor_line+1
-      if State.lines[new_cursor_line].mode == 'text' then
-        State.cursor1 = {line=new_cursor_line, pos=1}
-        break
-      end
-    end
-  end
-end
-
 function Text.pos_at_start_of_screen_line(State, loc1)
   Text.populate_screen_line_starting_pos(State, loc1.line)
   local line_cache = State.line_cache[loc1.line]
@@ -1112,39 +652,11 @@ function Text.pos_at_start_of_screen_line(State, loc1)
   assert(false)
 end
 
-function Text.pos_at_start_of_screen_lineB(State, loc1)
-  Text.populate_screen_line_starting_pos(State, loc1.line)
-  local line_cache = State.line_cache[loc1.line]
-  local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
-  Text.populate_screen_line_starting_posB(State, loc1.line, x)
-  for i=#line_cache.screen_line_starting_posB,1,-1 do
-    local sposB = line_cache.screen_line_starting_posB[i]
-    if sposB <= loc1.posB then
-      return sposB,i
-    end
-  end
-  assert(false)
-end
-
 function Text.cursor_at_final_screen_line(State)
   Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-  local line = State.lines[State.cursor1.line]
   local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos
 --?   print(screen_lines[#screen_lines], State.cursor1.pos)
-  if (not State.expanded and not line.expanded) or
-      line.dataB == nil then
-    return screen_lines[#screen_lines] <= State.cursor1.pos
-  end
-  if State.cursor1.pos then
-    -- ignore B side
-    return screen_lines[#screen_lines] <= State.cursor1.pos
-  end
-  assert(State.cursor1.posB)
-  local line_cache = State.line_cache[State.cursor1.line]
-  local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
-  Text.populate_screen_line_starting_posB(State, State.cursor1.line, x)
-  local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB
-  return screen_lines[#screen_lines] <= State.cursor1.posB
+  return screen_lines[#screen_lines] <= State.cursor1.pos
 end
 
 function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
@@ -1172,22 +684,17 @@ end
 
 -- should never modify State.cursor1
 function Text.snap_cursor_to_bottom_of_screen(State)
---?   print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
+--?   print('to2:', State.cursor1.line, State.cursor1.pos)
   local top2 = Text.to2(State, State.cursor1)
---?   print('to2: =>', top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
+--?   print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
   -- slide to start of screen line
-  if top2.screen_pos then
-    top2.screen_pos = 1
-  else
-    assert(top2.screen_posB)
-    top2.screen_posB = 1
-  end
---?   print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
+  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('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
   local y = App.screen.height - State.line_height
   -- duplicate some logic from love.draw
   while true do
---?     print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
+--?     print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
     if top2.line == 1 and top2.screen_line == 1 then break end
     if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
       local h = State.line_height
@@ -1212,7 +719,7 @@ 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.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
+--?   print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
   Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
 end
 
@@ -1221,116 +728,34 @@ function Text.in_line(State, line_index, x,y)
   local line_cache = State.line_cache[line_index]
   if line_cache.starty == nil then return false end  -- outside current page
   if y < line_cache.starty then return false end
-  local num_screen_lines = 0
-  if line_cache.startpos then
-    Text.populate_screen_line_starting_pos(State, line_index)
-    num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1
-  end
---?   print('#screenlines after A', num_screen_lines)
-  if line.dataB and (State.expanded or line.expanded) then
-    local x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding
-    Text.populate_screen_line_starting_posB(State, line_index, x)
---?     print('B:', x, #line_cache.screen_line_starting_posB)
-    if line_cache.startposB then
-      num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)  -- no +1; first screen line of B side overlaps with A side
-    else
-      num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1)  -- no +1; first screen line of B side overlaps with A side
-    end
-  end
---?   print('#screenlines after B', num_screen_lines)
-  return y < line_cache.starty + State.line_height*num_screen_lines
+  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)
 end
 
 -- convert mx,my in pixels to schema-1 coordinates
--- returns: pos, posB
--- scenarios:
---   line without B side
---   line with B side collapsed
---   line with B side expanded
---   line starting rendering in A side (startpos ~= nil)
---   line starting rendering in B side (startposB ~= nil)
---   my on final screen line of A side
---     mx to right of A side with no B side
---     mx to right of A side but left of B side
---     mx to right of B side
--- preconditions:
---  startpos xor startposB
---  expanded -> dataB
 function Text.to_pos_on_line(State, line_index, mx, my)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
   assert(my >= line_cache.starty)
   -- duplicate some logic from Text.draw
   local y = line_cache.starty
---?   print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos)  -- , #line_cache.screen_line_starting_posB)
-  if line_cache.startpos then
-    local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
-    for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
-      local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
-      local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
---?       print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
-      local nexty = y + State.line_height
-      if my < nexty then
-        -- On all wrapped screen lines but the final one, clicks past end of
-        -- line position cursor on final character of screen line.
-        -- (The final screen line positions past end of screen line as always.)
-        if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
---?           print('past end of non-final line; return')
-          return line_cache.screen_line_starting_pos[screen_line_index+1]-1
-        end
-        local s = string.sub(line.data, screen_line_starting_byte_offset)
---?         print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
-        local screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left)
-        if line.dataB == nil then
-          -- no B side
-          return screen_line_starting_pos + screen_line_posA - 1
-        end
-        if not State.expanded and not line.expanded then
-          -- B side is not expanded
-          return screen_line_starting_pos + screen_line_posA - 1
-        end
-        local lenA = utf8.len(s)
-        if screen_line_posA < lenA then
-          -- mx is within A side
-          return screen_line_starting_pos + screen_line_posA - 1
-        end
-        local max_xA = State.left+Text.x(s, lenA+1)
-        if mx < max_xA + AB_padding then
-          -- mx is in the space between A and B side
-          return screen_line_starting_pos + screen_line_posA - 1
-        end
-        mx = mx - max_xA - AB_padding
-        local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)
-        return nil, screen_line_posB
-      end
-      y = nexty
-    end
-  end
-  -- look in screen lines composed entirely of the B side
-  assert(State.expanded or line.expanded)
-  local start_screen_line_indexB
-  if line_cache.startposB then
-    start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)
-  else
-    start_screen_line_indexB = 2  -- skip the first line of side B, which we checked above
-  end
-  for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do
-    local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB]
-    local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB)
---?     print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
+  local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
+  for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
+    local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
+    local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
+--?     print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
     local nexty = y + State.line_height
     if my < nexty then
       -- On all wrapped screen lines but the final one, clicks past end of
       -- line position cursor on final character of screen line.
       -- (The final screen line positions past end of screen line as always.)
---?       print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))
-      if screen_line_indexB < #line_cache.screen_line_starting_posB and mx > State.left + Text.screen_line_widthB(State, line_index, screen_line_indexB) then
+      if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
 --?         print('past end of non-final line; return')
-        return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1
+        return line_cache.screen_line_starting_pos[screen_line_index+1]-1
       end
-      local s = string.sub(line.dataB, screen_line_starting_byte_offsetB)
---?       print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)
-      return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1
+      local s = string.sub(line.data, screen_line_starting_byte_offset)
+--?       print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
+      return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1
     end
     y = nexty
   end
@@ -1354,30 +779,6 @@ function Text.screen_line_width(State, line_index, i)
   return App.width(screen_line_text)
 end
 
-function Text.screen_line_widthB(State, line_index, i)
-  local line = State.lines[line_index]
-  local line_cache = State.line_cache[line_index]
-  local start_posB = line_cache.screen_line_starting_posB[i]
-  local start_offsetB = Text.offset(line.dataB, start_posB)
-  local screen_line
-  if i < #line_cache.screen_line_starting_posB then
---?     print('non-final', i)
-    local past_end_posB = line_cache.screen_line_starting_posB[i+1]
-    local past_end_offsetB = Text.offset(line.dataB, past_end_posB)
---?     print('between', start_offsetB, past_end_offsetB)
-    screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1)
-  else
---?     print('final', i)
---?     print('after', start_offsetB)
-    screen_line = string.sub(line.dataB, start_offsetB)
-  end
-  local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
---?   local result = App.width(screen_line_text)
---?   print('=>', result)
---?   return result
-  return App.width(screen_line_text)
-end
-
 function Text.screen_line_index(screen_line_starting_pos, pos)
   for i = #screen_line_starting_pos,1,-1 do
     if screen_line_starting_pos[i] <= pos then
@@ -1386,18 +787,6 @@ function Text.screen_line_index(screen_line_starting_pos, pos)
   end
 end
 
-function Text.screen_line_indexB(screen_line_starting_posB, posB)
-  if posB == nil then
-    return 0
-  end
-  assert(screen_line_starting_posB)
-  for i = #screen_line_starting_posB,1,-1 do
-    if screen_line_starting_posB[i] <= posB then
-      return i
-    end
-  end
-end
-
 -- convert x pixel coordinate to pos
 -- oblivious to wrapping
 -- result: 1 to len+1
@@ -1490,14 +879,6 @@ function Text.to2(State, loc1)
   if State.lines[loc1.line].mode == 'drawing' then
     return {line=loc1.line, screen_line=1, screen_pos=1}
   end
-  if loc1.pos then
-    return Text.to2A(State, loc1)
-  else
-    return Text.to2B(State, loc1)
-  end
-end
-
-function Text.to2A(State, loc1)
   local result = {line=loc1.line}
   local line_cache = State.line_cache[loc1.line]
   Text.populate_screen_line_starting_pos(State, loc1.line)
@@ -1513,33 +894,7 @@ function Text.to2A(State, loc1)
   return result
 end
 
-function Text.to2B(State, loc1)
-  local result = {line=loc1.line}
-  local line_cache = State.line_cache[loc1.line]
-  Text.populate_screen_line_starting_pos(State, loc1.line)
-  local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
-  Text.populate_screen_line_starting_posB(State, loc1.line, x)
-  for i=#line_cache.screen_line_starting_posB,1,-1 do
-    local sposB = line_cache.screen_line_starting_posB[i]
-    if sposB <= loc1.posB then
-      result.screen_lineB = i
-      result.screen_posB = loc1.posB - sposB + 1
-      break
-    end
-  end
-  assert(result.screen_posB)
-  return result
-end
-
 function Text.to1(State, loc2)
-  if loc2.screen_pos then
-    return Text.to1A(State, loc2)
-  else
-    return Text.to1B(State, loc2)
-  end
-end
-
-function Text.to1A(State, loc2)
   local result = {line=loc2.line, pos=loc2.screen_pos}
   if loc2.screen_line > 1 then
     result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
@@ -1547,12 +902,8 @@ function Text.to1A(State, loc2)
   return result
 end
 
-function Text.to1B(State, loc2)
-  local result = {line=loc2.line, posB=loc2.screen_posB}
-  if loc2.screen_lineB > 1 then
-    result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1
-  end
-  return result
+function Text.eq1(a, b)
+  return a.line == b.line and a.pos == b.pos
 end
 
 function Text.lt1(a, b)
@@ -1562,22 +913,17 @@ function Text.lt1(a, b)
   if a.line > b.line then
     return false
   end
-  -- A side < B side
-  if a.pos and not b.pos then
+  return a.pos < b.pos
+end
+
+function Text.le1(a, b)
+  if a.line < b.line then
     return true
   end
-  if not a.pos and b.pos then
+  if a.line > b.line then
     return false
   end
-  if a.pos then
-    return a.pos < b.pos
-  else
-    return a.posB < b.posB
-  end
-end
-
-function Text.le1(a, b)
-  return eq(a, b) or Text.lt1(a, b)
+  return a.pos <= b.pos
 end
 
 function Text.offset(s, pos1)
@@ -1591,49 +937,16 @@ function Text.offset(s, pos1)
 end
 
 function Text.previous_screen_line(State, loc2)
-  if loc2.screen_pos then
-    return Text.previous_screen_lineA(State, loc2)
-  else
-    return Text.previous_screen_lineB(State, loc2)
-  end
-end
-
-function Text.previous_screen_lineA(State, loc2)
   if loc2.screen_line > 1 then
     return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
   elseif loc2.line == 1 then
     return loc2
+  elseif State.lines[loc2.line-1].mode == 'drawing' then
+    return {line=loc2.line-1, screen_line=1, screen_pos=1}
   else
+    local l = State.lines[loc2.line-1]
     Text.populate_screen_line_starting_pos(State, loc2.line-1)
-    if State.lines[loc2.line-1].dataB == nil or
-        (not State.expanded and not State.lines[loc2.line-1].expanded) then
---?       print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)
-      return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
-    end
-    -- try to switch to B
-    local prev_line_cache = State.line_cache[loc2.line-1]
-    local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding
-    Text.populate_screen_line_starting_posB(State, loc2.line-1, x)
-    local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB
---?     print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x)
-    if #screen_line_starting_posB > 1 then
---?       print('c2')
-      return {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1}
-    else
---?       print('c3')
-      -- if there's only one screen line, assume it overlaps with A, so remain in A
-      return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
-    end
-  end
-end
-
-function Text.previous_screen_lineB(State, loc2)
-  if loc2.screen_lineB > 2 then  -- first screen line of B side overlaps with A side
-    return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1}
-  else
-    -- switch to A side
-    -- TODO: handle case where fold lands precisely at end of a new screen-line
-    return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}
+    return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
   end
 end
 
@@ -1666,8 +979,10 @@ function Text.tweak_screen_top_and_cursor(State)
 --?     print('too low')
     if Text.cursor_out_of_screen(State) then
 --?       print('tweak')
-      local pos,posB = Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5)
-      State.cursor1 = {line=State.screen_bottom1.line, pos=pos, posB=posB}
+      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),
+      }
     end
   end
 end
@@ -1704,9 +1019,7 @@ end
 
 function Text.clear_screen_line_cache(State, line_index)
   State.line_cache[line_index].fragments = nil
-  State.line_cache[line_index].fragmentsB = nil
   State.line_cache[line_index].screen_line_starting_pos = nil
-  State.line_cache[line_index].screen_line_starting_posB = nil
 end
 
 function trim(s)
@@ -1721,10 +1034,26 @@ function rtrim(s)
   return s:gsub('%s+$', '')
 end
 
-function starts_with(s, sub)
-  return s:find(sub, 1, --[[no escapes]] true) == 1
+function starts_with(s, prefix)
+  if #s < #prefix then
+    return false
+  end
+  for i=1,#prefix do
+    if s:sub(i,i) ~= prefix:sub(i,i) then
+      return false
+    end
+  end
+  return true
 end
 
-function ends_with(s, sub)
-  return s:reverse():find(sub:reverse(), 1, --[[no escapes]] true) == 1
+function ends_with(s, suffix)
+  if #s < #suffix then
+    return false
+  end
+  for i=0,#suffix-1 do
+    if s:sub(#s-i,#s-i) ~= suffix:sub(#suffix-i,#suffix-i) then
+      return false
+    end
+  end
+  return true
 end
diff --git a/source_text_tests.lua b/source_text_tests.lua
index 2385325..7a7d71c 100644
--- a/source_text_tests.lua
+++ b/source_text_tests.lua
@@ -1251,6 +1251,28 @@ function test_up_arrow_scrolls_up_by_one_line()
   App.screen.check(y, 'ghi', 'screen:3')
 end
 
+function test_up_arrow_scrolls_up_by_one_line_skipping_drawing()
+  -- display lines 3/4/5 with a drawing just off screen at line 2
+  App.screen.init{width=120, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', '```lines', '```', 'def', 'ghi', 'jkl'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=3, pos=1}
+  Editor_state.screen_top1 = {line=3, pos=1}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  local y = Editor_state.top
+  App.screen.check(y, 'def', 'baseline/screen:1')
+  y = y + Editor_state.line_height
+  App.screen.check(y, 'ghi', 'baseline/screen:2')
+  y = y + Editor_state.line_height
+  App.screen.check(y, 'jkl', 'baseline/screen:3')
+  -- after hitting the up arrow the screen scrolls up to previous text line
+  edit.run_after_keychord(Editor_state, 'up')
+  check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
+  check_eq(Editor_state.cursor1.line, 1, 'cursor')
+end
+
 function test_up_arrow_scrolls_up_by_one_screen_line()
   -- display lines starting from second screen line of a line
   App.screen.init{width=Editor_state.left+30, height=60}
diff --git a/text.lua b/text.lua
index 0e69057..464ab85 100644
--- a/text.lua
+++ b/text.lua
@@ -154,7 +154,6 @@ function Text.text_input(State, t)
   if State.cursor_y > App.screen.height - State.line_height then
     Text.populate_screen_line_starting_pos(State, State.cursor1.line)
     Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
---?     print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
   end
   record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
 end
@@ -224,9 +223,10 @@ function Text.keychord_press(State, chord)
       local line_cache = State.line_cache[#State.line_cache]
       State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
     elseif Text.lt1(State.cursor1, State.screen_top1) then
-      local top2 = Text.to2(State, State.screen_top1)
-      top2 = Text.previous_screen_line(State, top2, State.left, State.right)
-      State.screen_top1 = Text.to1(State, top2)
+      State.screen_top1 = {
+        line=State.cursor1.line,
+        pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+      }
       Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
     end
     Text.clear_screen_line_cache(State, State.cursor1.line)
@@ -431,9 +431,11 @@ function Text.up(State)
 --?     print('cursor pos is now '..tostring(State.cursor1.pos))
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
+    State.screen_top1 = {
+      line=State.cursor1.line,
+      pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+    }
+    Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
   end
 end
 
@@ -555,9 +557,11 @@ function Text.left(State)
     State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
-    local top2 = Text.to2(State, State.screen_top1)
-    top2 = Text.previous_screen_line(State, top2)
-    State.screen_top1 = Text.to1(State, top2)
+    State.screen_top1 = {
+      line=State.cursor1.line,
+      pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
+    }
+    Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
   end
 end
 
@@ -610,7 +614,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.screen_top1.posB, 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, State.screen_bottom1.line, State.screen_bottom1.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