about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Manual_tests.md2
-rw-r--r--README.md109
-rw-r--r--edit.lua254
-rw-r--r--file.lua136
-rw-r--r--main.lua12
-rw-r--r--reference.md17
-rw-r--r--run.lua4
-rw-r--r--select.lua10
-rw-r--r--source.lua4
-rw-r--r--text.lua193
-rw-r--r--text_tests.lua125
-rw-r--r--undo.lua10
12 files changed, 153 insertions, 723 deletions
diff --git a/Manual_tests.md b/Manual_tests.md
index 650bc76..3ee22c7 100644
--- a/Manual_tests.md
+++ b/Manual_tests.md
@@ -12,7 +12,7 @@ Initializing settings:
   - run with a filename on commandline, scroll around, quit; restart with new filename; window opens new filename with cursor up top
   - run editor, scroll around, move cursor to end of some line, quit; restart with new filename; window opens running the text editor in same position+dimensions
   - quit while running the text editor, restart; window opens running the text editor in same position+dimensions
-  - quit while editing source (color; no drawings; no selection), restart; window opens editing source in same position+dimensions
+  - quit while editing source (color; no selection), restart; window opens editing source in same position+dimensions
   - start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions
   - start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions
   - no log file; switching to source works
diff --git a/README.md b/README.md
index 690d94c..27ce06d 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
-# Plain text with lines
+# An editor for plain text.
+
+Not very useful by itself, but it's a fork of [lines.love](http://akkartik.name/lines.html)
+that you can take in other directions besides line drawings, while easily
+sharing patches between forks.
 
-An editor for plain text where you can also seamlessly insert line drawings.
 Designed above all to be easy to modify and give you early warning if your
 modifications break something.
 
-http://akkartik.name/lines.html
-
 ## Getting started
 
 Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and
@@ -17,13 +18,13 @@ optionally with a file path to edit.
 
 Alternatively, turn it into a .love file you can double-click on:
 ```
-$ zip -r /tmp/lines.love *.lua
+$ zip -r /tmp/text.love *.lua
 ```
 
-By default, lines.love reads/writes the file `lines.txt` in
+By default, it reads/writes the file `lines.txt` in
 [a directory relative to this app](https://love2d.org/wiki/love.filesystem.getSourceBaseDirectory).
 
-To open a different file, drop it on the lines.love window.
+To open a different file, drop it on the app window.
 
 ## Keyboard shortcuts
 
@@ -36,12 +37,7 @@ While editing text:
 * mouse drag or `shift` + movement to select text, `ctrl+a` to select all
 * `ctrl+e` to modify the sources
 
-For shortcuts while editing drawings, consult the online help. Either:
-* hover on a drawing and hit `ctrl+h`, or
-* click on a drawing to start a stroke and then press and hold `h` to see your
-  options at any point during a stroke.
-
-lines.love has been exclusively tested so far with a US keyboard layout. If
+Exclusively tested so far with a US keyboard layout. If
 you use a different layout, please let me know if things worked, or if you
 found anything amiss: http://akkartik.name/contact
 
@@ -52,74 +48,45 @@ found anything amiss: http://akkartik.name/contact
 * No support yet for right-to-left languages.
 
 * Undo/redo may be sluggish in large files. Large files may grow sluggish in
-  other ways. lines.love works well in all circumstances with files under
-  50KB.
+  other ways. Works well in all circumstances with files under 50KB.
 
 * If you kill the process, say by force-quitting because things things get
   sluggish, you can lose data.
 
-* The text cursor will always stay on the screen. This can have some strange
-  implications:
-
-    * A long series of drawings will get silently skipped when you hit
-      page-down, until a line of text can be showed on screen.
-    * If there's no line of text at the top of the file, you may not be able
-      to scroll back up to the top with page-up.
-
-  So far this app isn't really designed for drawing-heavy files. For now I'm
-  targeting mostly-text files with a few drawings mixed in.
-
-* No clipping yet for drawings. In particular, circles/squares/rectangles and
-  point labels can overflow a drawing.
-
-* If you ever see a crash when clicking on the mouse, it might be because a
-  mouse press and release need to happen in separate frames. Try pressing and
-  releasing more slowly and let me know if that helps or not. This is klunky,
-  sorry.
-
-* Touchpads can drag the mouse pointer using a light touch or a heavy click.
-  On Linux, drags using the light touch get interrupted when a key is pressed.
-  You'll have to press down to drag.
-
 * Can't scroll while selecting text with mouse.
 
 * No scrollbars yet. That stuff is hard.
 
 ## Mirrors and Forks
 
-Updates to lines.love can be downloaded from the following mirrors in addition
-to the website above:
-* https://git.sr.ht/~akkartik/lines.love
-* https://repo.or.cz/lines.love.git
-* https://tildegit.org/akkartik/lines.love
-* https://git.merveilles.town/akkartik/lines.love
-* https://git.tilde.institute/akkartik/lines.love
-* https://codeberg.org/akkartik/lines.love
-* https://github.com/akkartik/lines.love
-* https://notabug.org/akkartik/lines.love
-* https://pagure.io/lines.love
-* https://nest.pijul.com/akkartik/lines.love (using the Pijul version control system)
-
-Forks of lines.love are encouraged. If you show me your fork, I'll link to it
-here.
-
-* https://github.com/akkartik/lines-polygon-experiment -- an experiment that
-  uses separate shortcuts for regular polygons. `ctrl+3` for triangles,
-  `ctrl+4` for squares, etc.
-* https://git.sr.ht/~akkartik/text.love -- a stripped down version without
-  drawings; useful starting point for some forks
-* https://git.sr.ht/~akkartik/pensieve.love -- a note-taking app on an
-  infinite 2D surface. Still in development.
-* https://git.sr.ht/~akkartik/capture.love -- a blank-slate mode for the
-  note-taking app, so all the stuff pensieve.love puts on screen doesn't cause
-  you to forget what you came to write down.
-
-## Associated tools
-
-* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown
-  and (non-editable) SVG.
-* https://git.sr.ht/~akkartik/lines2html.love exports lines.love files to html
-  and inline SVG.
+This repo is a fork of [lines.love](http://akkartik.name/lines.html), an
+editor for plain text where you can also seamlessly insert line drawings.
+Updates to it can be downloaded from the following mirrors:
+
+* https://repo.or.cz/text.love.git
+* https://tildegit.org/akkartik/text.love
+* https://git.tilde.institute/akkartik/text.love
+* https://git.merveilles.town/akkartik/text.love
+* https://git.sr.ht/~akkartik/text.love
+* https://github.com/akkartik/text.love
+* https://codeberg.org/akkartik/text.love
+* https://notabug.org/akkartik/text.love
+* https://pagure.io/text.love
+* https://nest.pijul.com/akkartik/text.love (using the Pijul version control system)
+
+Further forks are encouraged. If you show me your fork, I'll link to it here.
+
+* https://git.sr.ht/~akkartik/view.love -- a stripped down version without
+  support for modifying files; useful starting point for some forks.
+* https://git.sr.ht/~akkartik/pong.love -- a fairly minimal example app that
+  can edit and debug its own source code.
+* https://git.sr.ht/~akkartik/template-live-editor -- a template for
+  building "free-wheeling" live programs (easy to fork, can be modified as
+  they run), with a text editor primitive.
+* https://git.sr.ht/~akkartik/luaML.love -- a free-wheeling 'browser' for a
+  Lua-based markup language built as a live program.
+* https://git.sr.ht/~akkartik/driver.love -- a programming environment for
+  modifying free-wheeling programs while they run.
 
 ## Feedback
 
diff --git a/edit.lua b/edit.lua
index 1fbe682..aa62ad3 100644
--- a/edit.lua
+++ b/edit.lua
@@ -1,50 +1,18 @@
 -- some constants people might like to tweak
 Text_color = {r=0, g=0, b=0}
 Cursor_color = {r=1, g=0, b=0}
-Stroke_color = {r=0, g=0, b=0}
-Current_stroke_color = {r=0.7, g=0.7, b=0.7}  -- in process of being drawn
-Current_name_background_color = {r=1, g=0, b=0, a=0.1}  -- name currently being edited
-Focus_stroke_color = {r=1, g=0, b=0}  -- what mouse is hovering over
 Highlight_color = {r=0.7, g=0.7, b=0.9}  -- selected text
-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}
 
 Margin_top = 15
 Margin_left = 25
 Margin_right = 25
 
-Drawing_padding_top = 10
-Drawing_padding_bottom = 10
-Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
-
-Same_point_distance = 4  -- pixel distance at which two points are considered the same
-
 edit = {}
 
 -- run in both tests and a real run
 function edit.initialize_state(top, left, right, font, font_height, line_height)  -- currently always draws to bottom of screen
   local result = {
-    -- a line is either text or a drawing
-    -- a text is a table with:
-    --    mode = 'text',
-    --    string data,
-    -- a drawing is a table with:
-    --    mode = 'drawing'
-    --    a (h)eight,
-    --    an array of points, and
-    --    an array of shapes
-    -- a shape is a table containing:
-    --    a mode
-    --    an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing)
-    --    an array vertices for mode 'polygon', 'rectangle', 'square'
-    --    p1, p2 for mode 'line'
-    --    center, radius for mode 'circle'
-    --    center, radius, start_angle, end_angle for mode 'arc'
-    -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide
-    -- The field names are carefully chosen so that switching modes in midstream
-    -- remembers previously entered points where that makes sense.
-    lines = {{mode='text', data=''}},  -- array of lines
+    lines = {{data=''}},  -- array of strings
 
     -- Lines can be too long to fit on screen, in which case they _wrap_ into
     -- multiple _screen lines_.
@@ -78,9 +46,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
     cursor_x = 0,
     cursor_y = 0,
 
-    current_drawing_mode = 'line',  -- one of the available shape modes
-    previous_drawing_mode = nil,  -- extra state for some ephemeral modes like moving/deleting/naming points
-
     font = font,
     font_height = font_height,
     line_height = line_height,
@@ -109,11 +74,9 @@ function edit.check_locs(State)
   --   throw away all cursor state entirely
   if edit.invalid1(State, State.screen_top1)
       or edit.invalid_cursor1(State)
-      or not edit.cursor_on_text(State)
       or not Text.le1(State.screen_top1, State.cursor1) then
     State.screen_top1 = {line=1, pos=1}
     State.cursor1 = {line=1, pos=1}
-    edit.put_cursor_on_next_text_line(State)
   end
 end
 
@@ -134,27 +97,8 @@ function edit.invalid_cursor1(State)
   return cursor1.pos > #State.lines[cursor1.line].data + 1
 end
 
-function edit.cursor_on_text(State)
-  return State.cursor1.line <= #State.lines
-      and State.lines[State.cursor1.line].mode == 'text'
-end
-
-function edit.put_cursor_on_next_text_line(State)
-  while true do
-    if State.cursor1.line >= #State.lines then
-      break
-    end
-    if State.lines[State.cursor1.line].mode == 'text' then
-      break
-    end
-    State.cursor1.line = State.cursor1.line+1
-    State.cursor1.pos = 1
-  end
-end
-
 -- return y drawn until
 function edit.draw(State)
-  State.button_handlers = {}
   love.graphics.setFont(State.font)
   App.color(Text_color)
   assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))
@@ -167,37 +111,13 @@ function edit.draw(State)
     local line = State.lines[line_index]
 --?     print('draw:', y, line_index, line)
     if y + State.line_height > App.screen.height then break end
-    if line.mode == 'text' then
---?       print('text.draw', y, line_index)
-      local startpos = 1
-      if line_index == State.screen_top1.line then
-        startpos = State.screen_top1.pos
-      end
-      if line.data == '' then
-        -- button to insert new drawing
-        button(State, 'draw', {x=State.left-Margin_left+4, y=y+4, w=12,h=12, bg={r=1,g=1,b=0},
-          icon = icon.insert_drawing,
-          onpress1 = function()
-                       Drawing.before = snapshot(State, line_index-1, line_index)
-                       table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
-                       table.insert(State.line_cache, line_index, {})
-                       if State.cursor1.line >= line_index then
-                         State.cursor1.line = State.cursor1.line+1
-                       end
-                       schedule_save(State)
-                       record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)})
-                     end,
-        })
-      end
-      y = Text.draw(State, line_index, y, startpos)
---?       print('=> y', y)
-    elseif line.mode == 'drawing' then
-      y = y+Drawing_padding_top
-      Drawing.draw(State, line_index, y)
-      y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
-    else
-      assert(false, ('unknown line mode %s'):format(line.mode))
+--?     print('text.draw', y, line_index)
+    local startpos = 1
+    if line_index == State.screen_top1.line then
+      startpos = State.screen_top1.pos
     end
+    y = Text.draw(State, line_index, y, startpos)
+--?     print('=> y', y)
   end
   if State.search_term then
     Text.draw_search_bar(State)
@@ -206,7 +126,6 @@ function edit.draw(State)
 end
 
 function edit.update(State, dt)
-  Drawing.update(State, dt)
   if State.next_save and State.next_save < Current_time then
     save_to_disk(State)
     State.next_save = nil
@@ -233,11 +152,6 @@ function edit.mouse_press(State, x,y, mouse_button)
   if State.search_term then return end
   State.mouse_down = mouse_button
 --?   print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
-  if mouse_press_consumed_by_any_button(State, x,y, mouse_button) then
-    -- press on a button and it returned 'true' to short-circuit
-    return
-  end
-
   if y < State.top then
     State.old_cursor1 = State.cursor1
     State.old_selection1 = State.selection1
@@ -250,35 +164,25 @@ function edit.mouse_press(State, x,y, mouse_button)
   end
 
   for line_index,line in ipairs(State.lines) do
-    if line.mode == 'text' then
-      if Text.in_line(State, line_index, x,y) then
-        -- delicate dance between cursor, selection and old cursor/selection
-        -- scenarios:
-        --  regular press+release: sets cursor, clears selection
-        --  shift press+release:
-        --    sets selection to old cursor if not set otherwise leaves it untouched
-        --    sets cursor
-        --  press and hold to start a selection: sets selection on press, cursor on release
-        --  press and hold, then press shift: ignore shift
-        --    i.e. mouse_release should never look at shift state
---?         print_and_log(('edit.mouse_press: in line %d'):format(line_index))
-        State.old_cursor1 = State.cursor1
-        State.old_selection1 = State.selection1
-        State.mousepress_shift = App.shift_down()
-        State.selection1 = {
-            line=line_index,
-            pos=Text.to_pos_on_line(State, line_index, x, y),
-        }
-        return
-      end
-    elseif line.mode == 'drawing' 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)
-        Drawing.mouse_press(State, line_index, x,y, mouse_button)
-        return
-      end
+    if Text.in_line(State, line_index, x,y) then
+      -- delicate dance between cursor, selection and old cursor/selection
+      -- scenarios:
+      --  regular press+release: sets cursor, clears selection
+      --  shift press+release:
+      --    sets selection to old cursor if not set otherwise leaves it untouched
+      --    sets cursor
+      --  press and hold to start a selection: sets selection on press, cursor on release
+      --  press and hold, then press shift: ignore shift
+      --    i.e. mouse_release should never look at shift state
+--?       print_and_log(('edit.mouse_press: in line %d'):format(line_index))
+      State.old_cursor1 = State.cursor1
+      State.old_selection1 = State.selection1
+      State.mousepress_shift = App.shift_down()
+      State.selection1 = {
+          line=line_index,
+          pos=Text.to_pos_on_line(State, line_index, x, y),
+      }
+      return
     end
   end
 
@@ -291,43 +195,30 @@ end
 
 function edit.mouse_release(State, x,y, mouse_button)
   if State.search_term then return end
---?   print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
+--?   print_and_log(('edit.mouse_release(%d,%d): cursor at %d,%d'):format(x,y, State.cursor1.line, State.cursor1.pos))
   State.mouse_down = nil
-  if State.lines.current_drawing then
-    Drawing.mouse_release(State, x,y, mouse_button)
-    schedule_save(State)
-    if Drawing.before then
-      record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
-      Drawing.before = nil
-    end
-  else
---?     print_and_log('edit.mouse_release: no current drawing')
-    if y < State.top then
-      State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
+  if y < State.top then
+    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
+    edit.clean_up_mouse_press(State)
+    return
+  end
+  for line_index,line in ipairs(State.lines) do
+    if Text.in_line(State, line_index, x,y) then
+--?       print_and_log(('edit.mouse_release: in line %d'):format(line_index))
+      State.cursor1 = {
+          line=line_index,
+          pos=Text.to_pos_on_line(State, line_index, x, y),
+      }
+--?       print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
       edit.clean_up_mouse_press(State)
       return
     end
-
-    for line_index,line in ipairs(State.lines) do
-      if line.mode == 'text' then
-        if Text.in_line(State, line_index, x,y) then
---?           print_and_log(('edit.mouse_release: in line %d'):format(line_index))
-          State.cursor1 = {
-              line=line_index,
-              pos=Text.to_pos_on_line(State, line_index, x, y),
-          }
---?           print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
-          edit.clean_up_mouse_press(State)
-          return
-        end
-      end
-    end
-
-    -- still here? mouse release is below all screen lines
-    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
+
+  -- still here? mouse release is below all screen lines
+  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
 
 function edit.clean_up_mouse_press(State)
@@ -347,7 +238,6 @@ end
 function edit.mouse_wheel_move(State, dx,dy)
   if dy > 0 then
     State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-    edit.put_cursor_on_next_text_line(State)
     for i=1,math.floor(dy) do
       Text.up(State)
     end
@@ -365,24 +255,14 @@ function edit.text_input(State, t)
   if State.search_term then
     State.search_term = State.search_term..t
     Text.search_next(State)
-  elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then
-    local before = snapshot(State, State.lines.current_drawing_index)
-    local drawing = State.lines.current_drawing
-    local p = drawing.points[drawing.pending.target_point]
-    p.name = p.name..t
-    record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
   else
-    local drawing_index, drawing = Drawing.current_drawing(State)
-    if drawing_index == nil then
-      Text.text_input(State, t)
-    end
+    Text.text_input(State, t)
   end
   schedule_save(State)
 end
 
 function edit.keychord_press(State, chord, key)
   if State.selection1.line and
-      not State.lines.current_drawing and
       -- printable character created using shift key => delete selection
       -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
       (not App.shift_down() or utf8.len(key) == 1) and
@@ -438,8 +318,6 @@ function edit.keychord_press(State, chord, key)
       State.selection1 = deepcopy(src.selection)
       patch(State.lines, event.after, event.before)
       patch_placeholders(State.line_cache, event.after, event.before)
-      -- invalidate various cached bits of lines
-      State.lines.current_drawing = nil
       -- if we're scrolling, reclaim all fragments to avoid memory leaks
       Text.redraw_all(State)
       schedule_save(State)
@@ -452,8 +330,6 @@ function edit.keychord_press(State, chord, key)
       State.cursor1 = deepcopy(src.cursor)
       State.selection1 = deepcopy(src.selection)
       patch(State.lines, event.before, event.after)
-      -- invalidate various cached bits of lines
-      State.lines.current_drawing = nil
       -- if we're scrolling, reclaim all fragments to avoid memory leaks
       Text.redraw_all(State)
       schedule_save(State)
@@ -492,43 +368,7 @@ function edit.keychord_press(State, chord, key)
     end
     schedule_save(State)
     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
-    local drawing_index, drawing = Drawing.current_drawing(State)
-    if drawing_index then
-      local before = snapshot(State, drawing_index)
-      Drawing.keychord_press(State, chord)
-      record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
-      schedule_save(State)
-    end
-  elseif chord == 'escape' and not App.mouse_down(1) then
-    for _,line in ipairs(State.lines) do
-      if line.mode == 'drawing' then
-        line.show_help = false
-      end
-    end
-  elseif State.lines.current_drawing and State.current_drawing_mode == 'name' then
-    if chord == 'return' then
-      State.current_drawing_mode = State.previous_drawing_mode
-      State.previous_drawing_mode = nil
-    else
-      local before = snapshot(State, State.lines.current_drawing_index)
-      local drawing = State.lines.current_drawing
-      local p = drawing.points[drawing.pending.target_point]
-      if chord == 'escape' then
-        p.name = nil
-        record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
-      elseif chord == 'backspace' then
-        local len = utf8.len(p.name)
-        if len > 0 then
-          local byte_offset = Text.offset(p.name, len-1)
-          if len == 1 then byte_offset = 0 end
-          p.name = string.sub(p.name, 1, byte_offset)
-          record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
-        end
-      end
-    end
-    schedule_save(State)
+  -- dispatch to text
   else
     Text.keychord_press(State, chord)
   end
diff --git a/file.lua b/file.lua
index 34be015..f7f832b 100644
--- a/file.lua
+++ b/file.lua
@@ -22,15 +22,11 @@ function load_from_file(infile)
     while true do
       local line = infile_next_line()
       if line == nil then break end
-      if line == '```lines' then  -- inflexible with whitespace since these files are always autogenerated
-        table.insert(result, load_drawing(infile_next_line))
-      else
-        table.insert(result, {mode='text', data=line})
-      end
+      table.insert(result, {data=line})
     end
   end
   if #result == 0 then
-    table.insert(result, {mode='text', data=''})
+    table.insert(result, {data=''})
   end
   return result
 end
@@ -41,85 +37,12 @@ function save_to_disk(State)
     error('failed to write to "'..State.filename..'"')
   end
   for _,line in ipairs(State.lines) do
-    if line.mode == 'drawing' then
-      store_drawing(outfile, line)
-    else
-      outfile:write(line.data)
-      outfile:write('\n')
-    end
+    outfile:write(line.data)
+    outfile:write('\n')
   end
   outfile:close()
 end
 
-function load_drawing(infile_next_line)
-  local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
-  while true do
-    local line = infile_next_line()
-    assert(line, 'drawing in file is incomplete')
-    if line == '```' then break end
-    local shape = json.decode(line)
-    if shape.mode == 'freehand' then
-      -- no changes needed
-    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
-      local name = shape.p1.name
-      shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.p1].name = name
-      name = shape.p2.name
-      shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.p2].name = name
-    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
-      for i,p in ipairs(shape.vertices) do
-        local name = p.name
-        shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)
-        drawing.points[shape.vertices[i]].name = name
-      end
-    elseif shape.mode == 'circle' or shape.mode == 'arc' then
-      local name = shape.center.name
-      shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.center].name = name
-    elseif shape.mode == 'deleted' then
-      -- ignore
-    else
-      assert(false, ('unknown drawing mode %s'):format(shape.mode))
-    end
-    table.insert(drawing.shapes, shape)
-  end
-  return drawing
-end
-
-function store_drawing(outfile, drawing)
-  outfile:write('```lines\n')
-  for _,shape in ipairs(drawing.shapes) do
-    if shape.mode == 'freehand' then
-      outfile:write(json.encode(shape))
-      outfile:write('\n')
-    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
-      local line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]})
-      outfile:write(line)
-      outfile:write('\n')
-    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
-      local obj = {mode=shape.mode, vertices={}}
-      for _,p in ipairs(shape.vertices) do
-        table.insert(obj.vertices, drawing.points[p])
-      end
-      local line = json.encode(obj)
-      outfile:write(line)
-      outfile:write('\n')
-    elseif shape.mode == 'circle' then
-      outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}))
-      outfile:write('\n')
-    elseif shape.mode == 'arc' then
-      outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle}))
-      outfile:write('\n')
-    elseif shape.mode == 'deleted' then
-      -- ignore
-    else
-      assert(false, ('unknown drawing mode %s'):format(shape.mode))
-    end
-  end
-  outfile:write('```\n')
-end
-
 -- for tests
 function load_array(a)
   local result = {}
@@ -128,61 +51,14 @@ function load_array(a)
   while true do
     i,line = next_line(a, i)
     if i == nil then break end
---?     print(line)
-    if line == '```lines' then  -- inflexible with whitespace since these files are always autogenerated
---?       print('inserting drawing')
-      i, drawing = load_drawing_from_array(next_line, a, i)
---?       print('i now', i)
-      table.insert(result, drawing)
-    else
---?       print('inserting text')
-      table.insert(result, {mode='text', data=line})
-    end
+    table.insert(result, {data=line})
   end
   if #result == 0 then
-    table.insert(result, {mode='text', data=''})
+    table.insert(result, {data=''})
   end
   return result
 end
 
-function load_drawing_from_array(iter, a, i)
-  local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
-  local line
-  while true do
-    i, line = iter(a, i)
-    assert(i, 'drawing in array is incomplete')
---?     print(i)
-    if line == '```' then break end
-    local shape = json.decode(line)
-    if shape.mode == 'freehand' then
-      -- no changes needed
-    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
-      local name = shape.p1.name
-      shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.p1].name = name
-      name = shape.p2.name
-      shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.p2].name = name
-    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
-      for i,p in ipairs(shape.vertices) do
-        local name = p.name
-        shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)
-        drawing.points[shape.vertices[i]].name = name
-      end
-    elseif shape.mode == 'circle' or shape.mode == 'arc' then
-      local name = shape.center.name
-      shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)
-      drawing.points[shape.center].name = name
-    elseif shape.mode == 'deleted' then
-      -- ignore
-    else
-      assert(false, ('unknown drawing mode %s'):format(shape.mode))
-    end
-    table.insert(drawing.shapes, shape)
-  end
-  return i, drawing
-end
-
 function is_absolute_path(path)
   local os_path_separator = package.config:sub(1,1)
   if os_path_separator == '/' then
diff --git a/main.lua b/main.lua
index 232308e..582f05a 100644
--- a/main.lua
+++ b/main.lua
@@ -24,13 +24,6 @@ load_file_from_source_or_save_directory('button.lua')
 -- both sides require (different parts of) the logging framework
 load_file_from_source_or_save_directory('log.lua')
 
--- both sides use drawings
-load_file_from_source_or_save_directory('icons.lua')
-load_file_from_source_or_save_directory('drawing.lua')
-  load_file_from_source_or_save_directory('geom.lua')
-  load_file_from_source_or_save_directory('help.lua')
-load_file_from_source_or_save_directory('drawing_tests.lua')
-
 -- but some files we want to only load sometimes
 function App.load()
   log_new('session')
@@ -70,6 +63,11 @@ function App.load()
         load_file_from_source_or_save_directory('source_undo.lua')
         load_file_from_source_or_save_directory('colorize.lua')
       load_file_from_source_or_save_directory('source_text_tests.lua')
+      load_file_from_source_or_save_directory('icons.lua')
+      load_file_from_source_or_save_directory('drawing.lua')
+        load_file_from_source_or_save_directory('geom.lua')
+        load_file_from_source_or_save_directory('help.lua')
+      load_file_from_source_or_save_directory('drawing_tests.lua')
     load_file_from_source_or_save_directory('source_tests.lua')
   elseif current_app_is_warning() then
   else
diff --git a/reference.md b/reference.md
index 1aadc41..972ab1d 100644
--- a/reference.md
+++ b/reference.md
@@ -199,9 +199,9 @@ early warning if you break something.
 
 * `state = edit.initialize_state(top, left, right, font, font_height, line_height)`
   -- returns an object that can be used to render an interactive editor widget
-  for text and line drawings starting at `y=top` on the app window, between
-  `x=left` and `x=right`. Wraps long lines at word boundaries where possible,
-  or in the middle of words (no hyphenation yet) when it must.
+  for text starting at `y=top` on the app window, between `x=left` and
+  `x=right`. Wraps long lines at word boundaries where possible, or in the
+  middle of words (no hyphenation yet) when it must.
 
 * `edit.quit()` -- calling this ensures any final edits are flushed to disk
   before the app exits.
@@ -248,20 +248,9 @@ Some constants that affect editor behavior:
 * `Margin_top`, `Margin_left`, `Margin_right` are integers in pixel units that
   affect where the editor is drawn on window (it always extends to bottom of
   window as needed)
-* `Drawing_padding_top` and `Drawing_padding_bottom` affect spacing around
-  drawings.
 
 * Various color constants are represented as tables with r/g/b keys:
   * `Text_color`, `Cursor_color`, `Highlight_color` for drawing text.
-  * `Stroke_color`, `Current_stroke_color` for line drawings.
-  * `Icon_color` affects the color of the little mode icon on the top right of
-    a drawing.
-  * `Current_name_background_color` manages the color when naming points using
-    `ctrl+n`.
-  * `Focus_stroke_color` affects the color of a point or line when you hover
-    over it.
-  * `Help_color` and `Help_background_color` affect the color of online help
-    within line drawings.
 
 ### clickable buttons
 
diff --git a/run.lua b/run.lua
index 2cdd892..307505d 100644
--- a/run.lua
+++ b/run.lua
@@ -34,7 +34,7 @@ function run.initialize(arg)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('lines.love - '..Editor_state.filename)
+  love.window.setTitle('text.love - '..Editor_state.filename)
 
 
 
@@ -128,7 +128,7 @@ function run.file_drop(file)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('lines.love - '..Editor_state.filename)
+  love.window.setTitle('text.love - '..Editor_state.filename)
 
 
 
diff --git a/select.lua b/select.lua
index b67dd16..e8df6f9 100644
--- a/select.lua
+++ b/select.lua
@@ -73,10 +73,8 @@ function Text.mouse_pos(State)
     return State.screen_top1.line, State.screen_top1.pos
   end
   for line_index,line in ipairs(State.lines) do
-    if line.mode == 'text' then
-      if Text.in_line(State, line_index, x,y) then
-        return line_index, Text.to_pos_on_line(State, line_index, x,y)
-      end
+    if Text.in_line(State, line_index, x,y) then
+      return line_index, Text.to_pos_on_line(State, line_index, x,y)
     end
   end
   local screen_bottom1 = Text.screen_bottom1(State)
@@ -158,9 +156,7 @@ function Text.selection(State)
   assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
   local result = {State.lines[minl].data:sub(min_offset)}
   for i=minl+1,maxl-1 do
-    if State.lines[i].mode == 'text' then
-      table.insert(result, State.lines[i].data)
-    end
+    table.insert(result, State.lines[i].data)
   end
   table.insert(result, State.lines[maxl].data:sub(1, max_offset-1))
   return table.concat(result, '\n')
diff --git a/source.lua b/source.lua
index d24957c..c85517d 100644
--- a/source.lua
+++ b/source.lua
@@ -74,7 +74,7 @@ function source.initialize()
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('lines.love - source - '..Editor_state.filename)
+  love.window.setTitle('text.love - source - '..Editor_state.filename)
 
 
 
@@ -207,7 +207,7 @@ function source.file_drop(file)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('lines.love - source')
+  love.window.setTitle('text.love - source')
 
 
 
diff --git a/text.lua b/text.lua
index c897f0b..5814693 100644
--- a/text.lua
+++ b/text.lua
@@ -82,7 +82,6 @@ end
 
 function Text.populate_screen_line_starting_pos(State, line_index)
   local line = State.lines[line_index]
-  if line.mode ~= 'text' then return end
   local line_cache = State.line_cache[line_index]
   if line_cache.screen_line_starting_pos then
     return
@@ -141,7 +140,6 @@ function Text.text_input(State, t)
 end
 
 function Text.insert_at_cursor(State, t)
-  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   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)
@@ -194,16 +192,11 @@ function Text.keychord_press(State, chord)
       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
-        table.remove(State.lines, State.cursor1.line-1)
-        table.remove(State.line_cache, State.cursor1.line-1)
-      else
-        -- join lines
-        State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
-        State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
-        table.remove(State.lines, State.cursor1.line)
-        table.remove(State.line_cache, State.cursor1.line)
-      end
+      -- join lines
+      State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
+      State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
+      table.remove(State.lines, State.cursor1.line)
+      table.remove(State.line_cache, State.cursor1.line)
       State.cursor1.line = State.cursor1.line-1
     end
     if State.screen_top1.line > #State.lines then
@@ -245,10 +238,8 @@ function Text.keychord_press(State, chord)
         -- no change to State.cursor1.pos
       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
-      end
+      -- join lines
+      State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
       table.remove(State.lines, State.cursor1.line+1)
       table.remove(State.line_cache, State.cursor1.line+1)
     end
@@ -342,7 +333,7 @@ end
 
 function Text.insert_return(State)
   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.lines, State.cursor1.line+1, {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)
@@ -365,15 +356,8 @@ function Text.starty(State, line_index)
   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
+    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
@@ -388,11 +372,7 @@ function Text.previous_screen_top1(State)
   local y = App.screen.height - State.line_height
   while y >= State.top do
     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[loc2.line].mode == 'drawing' then
-      y = y - Drawing_padding_height - Drawing.pixels(State.lines[loc2.line].h, State.width)
-    end
+    y = y - State.line_height
     loc2 = Text.previous_screen_line(State, loc2)
   end
   return Text.to1(State, loc2)
@@ -412,11 +392,7 @@ function Text.screen_bottom1(State)
   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
+    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
@@ -426,29 +402,24 @@ function Text.screen_bottom1(State)
 end
 
 function Text.up(State)
-  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
 --?   print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
   if screen_line_starting_pos == 1 then
 --?     print('cursor is at first screen line of its line')
     -- line is done; skip to previous text line
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line > 1 do
-      new_cursor_line = new_cursor_line-1
-      if State.lines[new_cursor_line].mode == 'text' then
---?         print('found previous text line')
-        State.cursor1 = {line=new_cursor_line, pos=nil}
-        Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-        -- previous text line found, pick its final screen line
---?         print('has multiple screen lines')
-        local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
---?         print(#screen_line_starting_pos)
-        screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
-        local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)
-        local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)
-        State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
-        break
-      end
+    if State.cursor1.line > 1 then
+      local new_cursor_line = State.cursor1.line-1
+--?       print('found previous text line')
+      State.cursor1 = {line=new_cursor_line, pos=nil}
+      Text.populate_screen_line_starting_pos(State, State.cursor1.line)
+      -- previous text line found, pick its final screen line
+--?       print('has multiple screen lines')
+      local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
+--?       print(#screen_line_starting_pos)
+      screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
+      local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)
+      local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)
+      State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
     end
   else
     -- move up one screen line in current line
@@ -469,23 +440,16 @@ function Text.up(State)
 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)
   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
 --?     print('cursor at final screen line of its line')
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line < #State.lines do
-      new_cursor_line = new_cursor_line+1
-      if State.lines[new_cursor_line].mode == 'text' then
-        State.cursor1 = {
-          line = new_cursor_line,
-          pos = Text.nearest_cursor_pos(State.font, State.lines[new_cursor_line].data, State.cursor_x, State.left),
-        }
---?         print(State.cursor1.pos)
-        break
-      end
+    if State.cursor1.line < #State.lines then
+      local new_cursor_line = State.cursor1.line+1
+      State.cursor1.line = new_cursor_line
+      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
     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)
@@ -589,21 +553,11 @@ function Text.match(s, pos, pat)
 end
 
 function Text.left(State)
-  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos > 1 then
     State.cursor1.pos = State.cursor1.pos-1
-  else
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line > 1 do
-      new_cursor_line = new_cursor_line-1
-      if State.lines[new_cursor_line].mode == 'text' then
-        State.cursor1 = {
-          line = new_cursor_line,
-          pos = utf8.len(State.lines[new_cursor_line].data) + 1,
-        }
-        break
-      end
-    end
+  elseif State.cursor1.line > 1 then
+    State.cursor1.line = State.cursor1.line-1
+    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
     State.screen_top1 = {
@@ -622,18 +576,11 @@ function Text.right(State)
 end
 
 function Text.right_without_scroll(State)
-  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
     State.cursor1.pos = State.cursor1.pos+1
-  else
-    local new_cursor_line = State.cursor1.line
-    while new_cursor_line <= #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
+  elseif State.cursor1.line <= #State.lines-1 then
+    State.cursor1.line = State.cursor1.line+1
+    State.cursor1.pos = 1
   end
 end
 
@@ -651,7 +598,6 @@ 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
@@ -667,21 +613,10 @@ 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
+  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)
@@ -692,26 +627,7 @@ function Text.cursor_at_final_screen_line(State)
 end
 
 function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
-  local y = State.top
-  while State.cursor1.line <= #State.lines do
-    if State.lines[State.cursor1.line].mode == 'text' then
-      break
-    end
---?     print('cursor skips', State.cursor1.line)
-    y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)
-    State.cursor1.line = State.cursor1.line + 1
-  end
-  if State.cursor1.pos == nil then
-    State.cursor1.pos = 1
-  end
-  -- hack: insert a text line at bottom of file if necessary
-  if State.cursor1.line > #State.lines then
-    assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
-    table.insert(State.lines, {mode='text', data=''})
-    table.insert(State.line_cache, {})
-  end
---?   print(y, App.screen.height, App.screen.height-State.line_height)
-  if y > App.screen.height - State.line_height then
+  if State.top > App.screen.height - State.line_height then
 --?     print('scroll up')
     Text.snap_cursor_to_bottom_of_screen(State)
   end
@@ -731,24 +647,11 @@ function Text.snap_cursor_to_bottom_of_screen(State)
   while true do
 --?     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
-      if y - h < State.top then
-        break
-      end
-      y = y - h
-    else
-      assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
-      assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
-      -- We currently can't draw partial drawings, so either skip it entirely
-      -- or not at all.
-      local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
-      if y - h < State.top then
-        break
-      end
---?       print('skipping drawing of height', h)
-      y = y - h
+    local h = State.line_height
+    if y - h < State.top then
+      break
     end
+    y = y - h
     top2 = Text.previous_screen_line(State, top2)
   end
 --?   print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
@@ -911,9 +814,6 @@ function Text.x(font, s, pos)
 end
 
 function Text.to2(State, loc1)
-  if State.lines[loc1.line].mode == 'drawing' then
-    return {line=loc1.line, screen_line=1, screen_pos=1}
-  end
   local result = {line=loc1.line}
   local line_cache = State.line_cache[loc1.line]
   Text.populate_screen_line_starting_pos(State, loc1.line)
@@ -979,8 +879,6 @@ function Text.previous_screen_line(State, loc2)
     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)
@@ -989,9 +887,6 @@ function Text.previous_screen_line(State, loc2)
 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
diff --git a/text_tests.lua b/text_tests.lua
index 9b34a24..9b3b60c 100644
--- a/text_tests.lua
+++ b/text_tests.lua
@@ -15,32 +15,6 @@ function test_initial_state()
   check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')
 end
 
-function test_click_to_create_drawing()
-  App.screen.init{width=800, height=600}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{}
-  Text.redraw_all(Editor_state)
-  edit.draw(Editor_state)
-  edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)
-  -- cursor skips drawing to always remain on text
-  check_eq(#Editor_state.lines, 2, '#lines')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor')
-end
-
-function test_backspace_to_delete_drawing()
-  -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'```lines', '```', ''}
-  Text.redraw_all(Editor_state)
-  -- cursor is on text as always (outside tests this will get initialized correctly)
-  Editor_state.cursor1.line = 2
-  -- backspacing deletes the drawing
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(#Editor_state.lines, 1, '#lines')
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
-end
-
 function test_backspace_from_start_of_final_line()
   -- display final line of text with cursor at start of it
   App.screen.init{width=120, height=60}
@@ -994,34 +968,6 @@ function test_pagedown()
   App.screen.check(y, 'ghi', 'screen:2')
 end
 
-function test_pagedown_skips_drawings()
-  -- some lines of text with a drawing intermixed
-  local drawing_width = 50
-  App.screen.init{width=Editor_state.left+drawing_width, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc',               -- height 15
-                                  '```lines', '```',   -- height 25
-                                  'def',               -- height 15
-                                  'ghi'}               -- height 15
-  Text.redraw_all(Editor_state)
-  check_eq(Editor_state.lines[2].mode, 'drawing', 'baseline/lines')
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  local drawing_height = Drawing_padding_height + drawing_width/2  -- default
-  -- initially the screen displays the first line and the drawing
-  -- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80px
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  -- after pagedown the screen draws the drawing up top
-  -- 15px margin + 10px margin + 25px drawing + 10px margin + 15px line3 = 75px < screen height 80px
-  edit.run_after_keychord(Editor_state, 'pagedown', 'pagedown')
-  check_eq(Editor_state.screen_top1.line, 2, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 3, 'cursor')
-  y = Editor_state.top + drawing_height
-  App.screen.check(y, 'def', 'screen:1')
-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}
@@ -1099,30 +1045,6 @@ function test_down_arrow_moves_cursor()
   App.screen.check(y, 'ghi', 'screen:3')
 end
 
-function test_down_arrow_skips_drawing()
-  -- some lines of text with a drawing intermixed
-  local drawing_width = 50
-  App.screen.init{width=Editor_state.left+drawing_width, height=100}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc',               -- height 15
-                                  '```lines', '```',   -- height 25
-                                  'ghi'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  local drawing_height = Drawing_padding_height + drawing_width/2  -- default
-  y = y + drawing_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  check(Editor_state.cursor_x, 'baseline/cursor_x')
-  -- after hitting the down arrow the cursor moves down by 2 lines, skipping the drawing
-  edit.run_after_keychord(Editor_state, 'down', 'down')
-  check_eq(Editor_state.cursor1.line, 3, 'cursor')
-end
-
 function test_down_arrow_scrolls_down_by_one_line()
   -- display the first three lines with the cursor on the bottom line
   App.screen.init{width=120, height=60}
@@ -1266,30 +1188,6 @@ function test_up_arrow_moves_cursor()
   App.screen.check(y, 'ghi', 'screen:3')
 end
 
-function test_up_arrow_skips_drawing()
-  -- some lines of text with a drawing intermixed
-  local drawing_width = 50
-  App.screen.init{width=Editor_state.left+drawing_width, height=100}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc',               -- height 15
-                                  '```lines', '```',   -- height 25
-                                  'ghi'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=3, pos=1}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  local drawing_height = Drawing_padding_height + drawing_width/2  -- default
-  y = y + drawing_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  check(Editor_state.cursor_x, 'baseline/cursor_x')
-  -- after hitting the up arrow the cursor moves up by 2 lines, skipping the drawing
-  edit.run_after_keychord(Editor_state, 'up', 'up')
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
-end
-
 function test_up_arrow_scrolls_up_by_one_line()
   -- display the lines 2/3/4 with the cursor on line 2
   App.screen.init{width=120, height=60}
@@ -1317,27 +1215,6 @@ 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}
-  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', '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}
@@ -2000,7 +1877,7 @@ end
 function test_search()
   App.screen.init{width=120, height=60}
   Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', '’deg'}  -- contains unicode quote in final line
+  Editor_state.lines = load_array{'abc', 'def', 'ghi', '’deg'}  -- contains unicode quote in final line
   Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
diff --git a/undo.lua b/undo.lua
index e5dea93..a41ba38 100644
--- a/undo.lua
+++ b/undo.lua
@@ -50,8 +50,6 @@ function snapshot(State, s,e)
     screen_top=deepcopy(State.screen_top1),
     selection=deepcopy(State.selection1),
     cursor=deepcopy(State.cursor1),
-    current_drawing_mode=Drawing_mode,
-    previous_drawing_mode=State.previous_drawing_mode,
     lines={},
     start_line=s,
     end_line=e,
@@ -60,13 +58,7 @@ function snapshot(State, s,e)
   -- deep copy lines without cached stuff like text fragments
   for i=s,e do
     local line = State.lines[i]
-    if line.mode == 'text' then
-      table.insert(event.lines, {mode='text', data=line.data})  -- I've forgotten: should we deepcopy(line.data)?
-    elseif line.mode == 'drawing' then
-      table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
-    else
-      assert(false, ('unknown line mode %s'):format(line.mode))
-    end
+    table.insert(event.lines, {data=line.data})  -- I've forgotten: should we deepcopy(line.data)?
   end
   return event
 end