about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md46
-rw-r--r--app.lua4
-rw-r--r--conf.lua3
-rw-r--r--drawing.lua6
-rw-r--r--edit.lua80
-rw-r--r--file.lua2
-rw-r--r--keychord.lua4
-rw-r--r--log.lua50
-rw-r--r--log_browser.lua6
-rw-r--r--main.lua28
-rw-r--r--reference.md3
-rw-r--r--run.lua20
-rw-r--r--run_tests.lua29
-rw-r--r--select.lua54
-rw-r--r--source.lua30
-rw-r--r--source_edit.lua26
-rw-r--r--source_text.lua2
-rw-r--r--source_text_tests.lua7
-rw-r--r--source_undo.lua8
-rw-r--r--text.lua137
-rw-r--r--text_tests2
-rw-r--r--text_tests.lua629
-rw-r--r--undo.lua88
23 files changed, 148 insertions, 1116 deletions
diff --git a/README.md b/README.md
index 0887f4a..bb4ab40 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# An editor for plain text.
+# A read-only viewer for plain text.
 
 [![0 dependencies!](https://0dependencies.dev/0dependencies.svg)](https://0dependencies.dev)
 
 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.
+that you can take in other directions, while easily sharing patches between
+forks.
 
 Designed above all to be easy to modify and give you early warning if your
 modifications break something.
@@ -19,7 +19,7 @@ optionally with a file path to edit.
 
 Alternatively, turn it into a .love file you can double-click on:
 ```
-$ zip -r /tmp/text.love *.lua
+$ zip -r /tmp/view.love *.lua
 ```
 
 By default, it reads/writes the file `lines.txt` in
@@ -56,33 +56,23 @@ found anything amiss: http://akkartik.name/contact
 
 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)
+Its immediate upstream is [text.love](https://git.sr.ht/~akkartik/text.love),
+a version without support for line drawings. Updates to it can be downloaded
+from the following mirrors:
+
+* https://git.sr.ht/~akkartik/view.love
+* https://repo.or.cz/view.love.git
+* https://tildegit.org/akkartik/view.love
+* https://git.tilde.institute/akkartik/view.love
+* https://git.merveilles.town/akkartik/view.love
+* https://github.com/akkartik/view.love
+* https://codeberg.org/akkartik/view.love
+* https://notabug.org/akkartik/view.love
+* https://pagure.io/view.love
+* https://nest.pijul.com/akkartik/view.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
 
 [Most appreciated.](http://akkartik.name/contact) Messages, PRs, patches,
diff --git a/app.lua b/app.lua
index 1bec1f0..2375ff9 100644
--- a/app.lua
+++ b/app.lua
@@ -428,9 +428,9 @@ function App.disable_tests()
       -- love.keyboard.isDown doesn't work on Android, so emulate it using
       -- keypressed and keyreleased events
       if name == 'keypressed' then
-        love.handlers[name] = function(key, scancode, isrepeat)
+        love.handlers[name] = function(key, scancode, is_repeat)
                                 Keys_down[key] = true
-                                return App.keypressed(key, scancode, isrepeat)
+                                return App.keypressed(key, scancode, is_repeat)
                               end
       elseif name == 'keyreleased' then
         love.handlers[name] = function(key, scancode)
diff --git a/conf.lua b/conf.lua
new file mode 100644
index 0000000..d5b3903
--- /dev/null
+++ b/conf.lua
@@ -0,0 +1,3 @@
+function love.conf(t)
+  t.identity = 'view'
+end
diff --git a/drawing.lua b/drawing.lua
index 92e3d5f..b74855a 100644
--- a/drawing.lua
+++ b/drawing.lua
@@ -221,7 +221,7 @@ function Drawing.in_drawing(State, line_index, x,y, left,right)
   return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
 end
 
-function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)
+function Drawing.mouse_press(State, drawing_index, x,y, mouse_button, is_touch, presses)
   local drawing = State.lines[drawing_index]
   local starty = Text.starty(State, drawing_index)
   local cx = Drawing.coord(x-State.left, State.width)
@@ -300,7 +300,7 @@ function Drawing.relax_constraints(drawing, p)
   end
 end
 
-function Drawing.mouse_release(State, x,y, mouse_button)
+function Drawing.mouse_release(State, x,y, mouse_button, is_touch, presses)
   if State.current_drawing_mode == 'move' then
     State.current_drawing_mode = State.previous_drawing_mode
     State.previous_drawing_mode = nil
@@ -396,7 +396,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
   end
 end
 
-function Drawing.keychord_press(State, chord)
+function Drawing.keychord_press(State, chord, key, scancode, is_repeat)
   if chord == 'C-p' and not App.mouse_down(1) then
     State.current_drawing_mode = 'freehand'
   elseif App.mouse_down(1) and chord == 'l' then
diff --git a/edit.lua b/edit.lua
index 97860b7..54b8aa9 100644
--- a/edit.lua
+++ b/edit.lua
@@ -56,11 +56,6 @@ function edit.initialize_state(top, left, right, font, font_height, line_height)
     width = right-left,
 
     filename = love.filesystem.getSourceBaseDirectory()..'/lines.txt',  -- '/' should work even on Windows
-    next_save = nil,
-
-    -- undo
-    history = {},
-    next_history = 1,
 
     -- search
     search_term = nil,
@@ -124,28 +119,12 @@ function edit.draw(State)
 end
 
 function edit.update(State, dt)
-  if State.next_save and State.next_save < Current_time then
-    save_to_disk(State)
-    State.next_save = nil
-  end
-end
-
-function schedule_save(State)
-  if State.next_save == nil then
-    State.next_save = Current_time + 3  -- short enough that you're likely to still remember what you did
-  end
 end
 
 function edit.quit(State)
-  -- make sure to save before quitting
-  if State.next_save then
-    save_to_disk(State)
-    -- 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)
+function edit.mouse_press(State, x,y, mouse_button, is_touch, presses)
   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))
@@ -190,7 +169,7 @@ function edit.mouse_press(State, x,y, mouse_button)
   State.selection1 = Text.final_loc_on_screen(State)
 end
 
-function edit.mouse_release(State, x,y, mouse_button)
+function edit.mouse_release(State, x,y, mouse_button, is_touch, presses)
   if State.search_term then return end
 --?   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
@@ -251,13 +230,10 @@ function edit.text_input(State, t)
   if State.search_term then
     State.search_term = State.search_term..t
     Text.search_next(State)
-  else
-    Text.text_input(State, t)
   end
-  schedule_save(State)
 end
 
-function edit.keychord_press(State, chord, key)
+function edit.keychord_press(State, chord, key, scancode, is_repeat)
   if State.selection1.line and
       -- printable character created using shift key => delete selection
       -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
@@ -309,29 +285,6 @@ function edit.keychord_press(State, chord, key)
   elseif chord == 'C-0' then
     edit.update_font_settings(State, 20)
     Text.redraw_all(State)
-  -- undo
-  elseif chord == 'C-z' then
-    local event = undo_event(State)
-    if event then
-      local src = event.before
-      State.screen_top1 = deepcopy(src.screen_top)
-      State.cursor1 = deepcopy(src.cursor)
-      State.selection1 = deepcopy(src.selection)
-      patch(State.lines, event.after, event.before)
-      Text.redraw_all(State)  -- if we're scrolling, reclaim all line caches to avoid memory leaks
-      schedule_save(State)
-    end
-  elseif chord == 'C-y' then
-    local event = redo_event(State)
-    if event then
-      local src = event.after
-      State.screen_top1 = deepcopy(src.screen_top)
-      State.cursor1 = deepcopy(src.cursor)
-      State.selection1 = deepcopy(src.selection)
-      patch(State.lines, event.before, event.after)
-      Text.redraw_all(State)  -- if we're scrolling, reclaim all line caches to avoid memory leaks
-      schedule_save(State)
-    end
   -- clipboard
   elseif chord == 'C-a' then
     State.selection1 = {line=1, pos=1}
@@ -341,33 +294,8 @@ function edit.keychord_press(State, chord, key)
     if s then
       App.set_clipboard(s)
     end
-  elseif chord == 'C-x' then
-    local s = Text.cut_selection_and_record_undo_event(State)
-    if s then
-      App.set_clipboard(s)
-    end
-    schedule_save(State)
-  elseif chord == 'C-v' then
-    -- We don't have a good sense of when to scroll, so we'll be conservative
-    -- and sometimes scroll when we didn't quite need to.
-    local before_line = State.cursor1.line
-    local before = snapshot(State, before_line)
-    local clipboard_data = App.get_clipboard()
-    for _,code in utf8.codes(clipboard_data) do
-      local c = utf8.char(code)
-      if c == '\n' then
-        Text.insert_return(State)
-      else
-        Text.insert_at_cursor(State, c)
-      end
-    end
-    if Text.cursor_out_of_screen(State) then
-      Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
-    end
-    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
-    schedule_save(State)
   else
-    Text.keychord_press(State, chord)
+    Text.keychord_press(State, chord, key, scancode, is_repeat)
   end
 end
 
diff --git a/file.lua b/file.lua
index f7f832b..028ffb4 100644
--- a/file.lua
+++ b/file.lua
@@ -47,7 +47,7 @@ end
 function load_array(a)
   local result = {}
   local next_line = ipairs(a)
-  local i,line,drawing = 0, ''
+  local i,line = 0, ''
   while true do
     i,line = next_line(a, i)
     if i == nil then break end
diff --git a/keychord.lua b/keychord.lua
index f1b6a59..5de899d 100644
--- a/keychord.lua
+++ b/keychord.lua
@@ -2,13 +2,13 @@
 
 Modifiers = {'lctrl', 'rctrl', 'lalt', 'ralt', 'lshift', 'rshift', 'lgui', 'rgui'}
 
-function App.keypressed(key, scancode, isrepeat)
+function App.keypressed(key, scancode, is_repeat)
   if array.find(Modifiers, key) then
     -- do nothing when the modifier is pressed
     return
   end
   -- include the modifier(s) when the non-modifer is pressed
-  App.keychord_press(App.combine_modifiers(key), key)
+  App.keychord_press(App.combine_modifiers(key), key, scancode, is_repeat)
 end
 
 function App.combine_modifiers(key)
diff --git a/log.lua b/log.lua
index 8903e08..4e428d8 100644
--- a/log.lua
+++ b/log.lua
@@ -1,38 +1,38 @@
 function log(stack_frame_index, obj)
-	local info = debug.getinfo(stack_frame_index, 'Sl')
-	local msg
-	if type(obj) == 'string' then
-		msg = obj
-	else
-		msg = json.encode(obj)
-	end
-	love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n')
+  local info = debug.getinfo(stack_frame_index, 'Sl')
+  local msg
+  if type(obj) == 'string' then
+    msg = obj
+  else
+    msg = json.encode(obj)
+  end
+  love.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n')
 end
 
 -- for section delimiters we'll use specific Unicode box characters
 function log_start(name, stack_frame_index)
-	if stack_frame_index == nil then
-		stack_frame_index = 3
-	end
-	-- I'd like to use the unicode character \u{250c} here, but it doesn't work
-	-- in OpenBSD.
-	log(stack_frame_index, '[ u250c ' .. name)
+  if stack_frame_index == nil then
+    stack_frame_index = 3
+  end
+  -- I'd like to use the unicode character \u{250c} here, but it doesn't work
+  -- in OpenBSD.
+  log(stack_frame_index, '[ u250c ' .. name)
 end
 function log_end(name, stack_frame_index)
-	if stack_frame_index == nil then
-		stack_frame_index = 3
-	end
-	-- I'd like to use the unicode character \u{2518} here, but it doesn't work
-	-- in OpenBSD.
-	log(stack_frame_index, '] u2518 ' .. name)
+  if stack_frame_index == nil then
+    stack_frame_index = 3
+  end
+  -- I'd like to use the unicode character \u{2518} here, but it doesn't work
+  -- in OpenBSD.
+  log(stack_frame_index, '] u2518 ' .. name)
 end
 
 function log_new(name, stack_frame_index)
-	if stack_frame_index == nil then
-		stack_frame_index = 4
-	end
-	log_end(name, stack_frame_index)
-	log_start(name, stack_frame_index)
+  if stack_frame_index == nil then
+    stack_frame_index = 4
+  end
+  log_end(name, stack_frame_index)
+  log_start(name, stack_frame_index)
 end
 
 -- rendering graphical objects within sections/boxes
diff --git a/log_browser.lua b/log_browser.lua
index 6e7e6db..fae9d6d 100644
--- a/log_browser.lua
+++ b/log_browser.lua
@@ -183,7 +183,7 @@ end
 function log_browser.quit(State)
 end
 
-function log_browser.mouse_press(State, x,y, mouse_button)
+function log_browser.mouse_press(State, x,y, mouse_button, is_touch, presses)
   local line_index = log_browser.line_index(State, x,y)
   if line_index == nil then
     -- below lower margin
@@ -249,7 +249,7 @@ function log_browser.line_index(State, mx,my)
   end
 end
 
-function log_browser.mouse_release(State, x,y, mouse_button)
+function log_browser.mouse_release(State, x,y, mouse_button, is_touch, presses)
 end
 
 function log_browser.mouse_wheel_move(State, dx,dy)
@@ -267,7 +267,7 @@ end
 function log_browser.text_input(State, t)
 end
 
-function log_browser.keychord_press(State, chord, key)
+function log_browser.keychord_press(State, chord, key, scancode, is_repeat)
   -- move
   if chord == 'up' then
     log_browser.up(State)
diff --git a/main.lua b/main.lua
index e7d35f4..83049c2 100644
--- a/main.lua
+++ b/main.lua
@@ -113,15 +113,15 @@ function check_love_version_for_tests()
   end
 end
 
-function App.initialize(arg)
+function App.initialize(arg, unfiltered_arg)
   love.keyboard.setKeyRepeat(true)
 
   love.graphics.setBackgroundColor(1,1,1)
 
   if Current_app == 'run' then
-    run.initialize(arg)
+    run.initialize(arg, unfiltered_arg)
   elseif Current_app == 'source' then
-    source.initialize(arg)
+    source.initialize(arg, unfiltered_arg)
   elseif current_app_is_warning() then
   else
     assert(false, 'unknown app "'..Current_app..'"')
@@ -207,7 +207,7 @@ function App.update(dt)
   end
 end
 
-function App.keychord_press(chord, key)
+function App.keychord_press(chord, key, scancode, is_repeat)
   -- ignore events for some time after window in focus (mostly alt-tab)
   if Current_time < Last_focus_time + 0.01 then
     return
@@ -251,9 +251,9 @@ function App.keychord_press(chord, key)
     return
   end
   if Current_app == 'run' then
-    if run.keychord_press then run.keychord_press(chord, key) end
+    if run.keychord_press then run.keychord_press(chord, key, scancode, is_repeat) end
   elseif Current_app == 'source' then
-    if source.keychord_press then source.keychord_press(chord, key) end
+    if source.keychord_press then source.keychord_press(chord, key, scancode, is_repeat) end
   else
     assert(false, 'unknown app "'..Current_app..'"')
   end
@@ -293,24 +293,24 @@ function App.keyreleased(key, scancode)
   end
 end
 
-function App.mousepressed(x,y, mouse_button)
+function App.mousepressed(x,y, mouse_button, is_touch, presses)
   if current_app_is_warning() then return end
 --?   print('mouse press', x,y)
   if Current_app == 'run' then
-    if run.mouse_press then run.mouse_press(x,y, mouse_button) end
+    if run.mouse_press then run.mouse_press(x,y, mouse_button, is_touch, presses) end
   elseif Current_app == 'source' then
-    if source.mouse_press then source.mouse_press(x,y, mouse_button) end
+    if source.mouse_press then source.mouse_press(x,y, mouse_button, is_touch, presses) end
   else
     assert(false, 'unknown app "'..Current_app..'"')
   end
 end
 
-function App.mousereleased(x,y, mouse_button)
+function App.mousereleased(x,y, mouse_button, is_touch, presses)
   if current_app_is_warning() then return end
   if Current_app == 'run' then
-    if run.mouse_release then run.mouse_release(x,y, mouse_button) end
+    if run.mouse_release then run.mouse_release(x,y, mouse_button, is_touch, presses) end
   elseif Current_app == 'source' then
-    if source.mouse_release then source.mouse_release(x,y, mouse_button) end
+    if source.mouse_release then source.mouse_release(x,y, mouse_button, is_touch, presses) end
   else
     assert(false, 'unknown app "'..Current_app..'"')
   end
@@ -319,9 +319,9 @@ end
 function App.mousemoved(x,y, dx,dy, is_touch)
   if current_app_is_warning() then return end
   if Current_app == 'run' then
-    if run.mouse_move then run.mouse_move(dx,dy) end
+    if run.mouse_move then run.mouse_move(x,y, dx,dy, is_touch) end
   elseif Current_app == 'source' then
-    if source.mouse_move then source.mouse_move(dx,dy) end
+    if source.mouse_move then source.mouse_move(x,y, dx,dy, is_touch) end
   else
     assert(false, 'unknown app "'..Current_app..'"')
   end
diff --git a/reference.md b/reference.md
index 972ab1d..80309c3 100644
--- a/reference.md
+++ b/reference.md
@@ -203,9 +203,6 @@ early warning if you break something.
   `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.
-
 * `edit.draw(state)` -- call this from `App.draw` to display the current
   editor state on the app window as requested in the call to
   `edit.initialize_state` that created `state`.
diff --git a/run.lua b/run.lua
index 9a7051d..e317733 100644
--- a/run.lua
+++ b/run.lua
@@ -11,7 +11,7 @@ function run.initialize_globals()
 end
 
 -- called only for real run
-function run.initialize(arg)
+function run.initialize(arg, unfiltered_arg)
   log_new('run')
   if Settings then
     run.load_settings()
@@ -34,7 +34,7 @@ function run.initialize(arg)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('text.love - '..Editor_state.filename)
+  love.window.setTitle('view.love - '..Editor_state.filename)
 
 
 
@@ -100,7 +100,7 @@ function run.initialize_window_geometry()
   App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
 end
 
-function run.resize(w, h)
+function run.resize(w,h)
 --?   print(("Window resized to width: %d and height: %d."):format(w, h))
   App.screen.width, App.screen.height = w, h
   Text.redraw_all(Editor_state)
@@ -128,7 +128,7 @@ function run.file_drop(file)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('text.love - '..Editor_state.filename)
+  love.window.setTitle('view.love - '..Editor_state.filename)
 
 
 
@@ -166,15 +166,15 @@ function absolutize(path)
   return path
 end
 
-function run.mouse_press(x,y, mouse_button)
+function run.mouse_press(x,y, mouse_button, is_touch, presses)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
   love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
-  return edit.mouse_press(Editor_state, x,y, mouse_button)
+  return edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses)
 end
 
-function run.mouse_release(x,y, mouse_button)
+function run.mouse_release(x,y, mouse_button, is_touch, presses)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
-  return edit.mouse_release(Editor_state, x,y, mouse_button)
+  return edit.mouse_release(Editor_state, x,y, mouse_button, is_touch, presses)
 end
 
 function run.mouse_wheel_move(dx,dy)
@@ -187,9 +187,9 @@ function run.text_input(t)
   return edit.text_input(Editor_state, t)
 end
 
-function run.keychord_press(chord, key)
+function run.keychord_press(chord, key, scancode, is_repeat)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
-  return edit.keychord_press(Editor_state, chord, key)
+  return edit.keychord_press(Editor_state, chord, key, scancode, is_repeat)
 end
 
 function run.key_release(key, scancode)
diff --git a/run_tests.lua b/run_tests.lua
index c5c4eff..bea2807 100644
--- a/run_tests.lua
+++ b/run_tests.lua
@@ -43,32 +43,3 @@ function test_drop_file()
   check_eq(Editor_state.lines[3].data, 'ghi', 'lines:3')
   edit.draw(Editor_state)
 end
-
-function test_drop_file_saves_previous()
-  App.screen.init{width=Editor_state.left+300, height=300}
-  -- initially editing a file called foo that hasn't been saved to filesystem yet
-  Editor_state.lines = load_array{'abc', 'def'}
-  Editor_state.filename = 'foo'
-  schedule_save(Editor_state)
-  -- now drag a new file bar from the filesystem
-  App.filesystem['bar'] = 'abc\ndef\nghi\n'
-  local fake_dropped_file = {
-    opened = false,
-    getFilename = function(self)
-                    return 'bar'
-                  end,
-    open = function(self)
-             self.opened = true
-           end,
-    lines = function(self)
-              assert(self.opened)
-              return App.filesystem['bar']:gmatch('[^\n]+')
-            end,
-    close = function(self)
-              self.opened = false
-            end,
-  }
-  App.filedropped(fake_dropped_file)
-  -- filesystem now contains a file called foo
-  check_eq(App.filesystem['foo'], 'abc\ndef\n', 'check')
-end
diff --git a/select.lua b/select.lua
index 78affdc..018ddef 100644
--- a/select.lua
+++ b/select.lua
@@ -79,60 +79,6 @@ function Text.mouse_pos(State)
   return screen_bottom1.line, Text.pos_at_end_of_screen_line(State, screen_bottom1)
 end
 
-function Text.cut_selection_and_record_undo_event(State)
-  if State.selection1.line == nil then return end
-  local result = Text.selection(State)
-  Text.delete_selection_and_record_undo_event(State)
-  return result
-end
-
-function Text.delete_selection_and_record_undo_event(State)
-  if State.selection1.line == nil then return end
-  local minl,maxl = minmax(State.selection1.line, State.cursor1.line)
-  local before = snapshot(State, minl, maxl)
-  Text.delete_selection_without_undo(State)
-  record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-end
-
-function Text.delete_selection_without_undo(State)
-  if State.selection1.line == nil then return end
-  -- min,max = sorted(State.selection1,State.cursor1)
-  local minl,minp = State.selection1.line,State.selection1.pos
-  local maxl,maxp = State.cursor1.line,State.cursor1.pos
-  if minl > maxl then
-    minl,maxl = maxl,minl
-    minp,maxp = maxp,minp
-  elseif minl == maxl then
-    if minp > maxp then
-      minp,maxp = maxp,minp
-    end
-  end
-  -- update State.cursor1 and State.selection1
-  State.cursor1.line = minl
-  State.cursor1.pos = minp
-  if Text.lt1(State.cursor1, State.screen_top1) then
-    State.screen_top1.line = State.cursor1.line
-    State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
-  end
-  State.selection1 = {}
-  -- delete everything between min (inclusive) and max (exclusive)
-  Text.clear_screen_line_cache(State, minl)
-  local min_offset = Text.offset(State.lines[minl].data, minp)
-  local max_offset = Text.offset(State.lines[maxl].data, maxp)
-  if minl == maxl then
---?     print('minl == maxl')
-    State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
-    return
-  end
-  assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
-  local rhs = State.lines[maxl].data:sub(max_offset)
-  for i=maxl,minl+1,-1 do
-    table.remove(State.lines, i)
-    table.remove(State.line_cache, i)
-  end
-  State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs
-end
-
 function Text.selection(State)
   if State.selection1.line == nil then return end
   -- min,max = sorted(State.selection1,State.cursor1)
diff --git a/source.lua b/source.lua
index 0644195..a998b67 100644
--- a/source.lua
+++ b/source.lua
@@ -56,7 +56,7 @@ function source.initialize_globals()
 end
 
 -- called only for real run
-function source.initialize()
+function source.initialize(arg, unfiltered_arg)
   log_new('source')
   if Settings and Settings.source then
     source.load_settings()
@@ -74,7 +74,7 @@ function source.initialize()
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('text.love - source - '..Editor_state.filename)
+  love.window.setTitle('view.love - source - '..Editor_state.filename)
 
 
 
@@ -173,7 +173,7 @@ function source.initialize_window_geometry()
   App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)
 end
 
-function source.resize(w, h)
+function source.resize(w,h)
 --?   print(("Window resized to width: %d and height: %d."):format(w, h))
   App.screen.width, App.screen.height = w, h
   Text.redraw_all(Editor_state)
@@ -207,7 +207,7 @@ function source.file_drop(file)
 
 
   -- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708
-  love.window.setTitle('text.love - source')
+  love.window.setTitle('view.love - source')
 
 
 
@@ -283,7 +283,7 @@ function source.settings()
   }
 end
 
-function source.mouse_press(x,y, mouse_button)
+function source.mouse_press(x,y, mouse_button, is_touch, presses)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
   love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
 --?   print('mouse click', x, y)
@@ -291,7 +291,7 @@ function source.mouse_press(x,y, mouse_button)
 --?   print(Log_browser_state.left, Log_browser_state.right)
   if Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then
     -- send click to buttons
-    edit.mouse_press(Editor_state, x,y, mouse_button)
+    edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses)
     return
   end
   if x < Editor_state.right + Margin_right then
@@ -300,23 +300,23 @@ function source.mouse_press(x,y, mouse_button)
       Focus = 'edit'
       return
     end
-    edit.mouse_press(Editor_state, x,y, mouse_button)
+    edit.mouse_press(Editor_state, x,y, mouse_button, is_touch, presses)
   elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then
 --?     print('click on log_browser side')
     if Focus ~= 'log_browser' then
       Focus = 'log_browser'
       return
     end
-    log_browser.mouse_press(Log_browser_state, x,y, mouse_button)
+    log_browser.mouse_press(Log_browser_state, x,y, mouse_button, is_touch, presses)
   end
 end
 
-function source.mouse_release(x,y, mouse_button)
+function source.mouse_release(x,y, mouse_button, is_touch, presses)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
   if Focus == 'edit' then
-    return edit.mouse_release(Editor_state, x,y, mouse_button)
+    return edit.mouse_release(Editor_state, x,y, mouse_button, is_touch, presses)
   else
-    return log_browser.mouse_release(Log_browser_state, x,y, mouse_button)
+    return log_browser.mouse_release(Log_browser_state, x,y, mouse_button, is_touch, presses)
   end
 end
 
@@ -342,7 +342,7 @@ function source.text_input(t)
   end
 end
 
-function source.keychord_press(chord, key)
+function source.keychord_press(chord, key, scancode, is_repeat)
   Cursor_time = 0  -- ensure cursor is visible immediately after it moves
 --?   print('source keychord')
   if Show_file_navigator then
@@ -382,9 +382,9 @@ function source.keychord_press(chord, key)
     return
   end
   if Focus == 'edit' then
-    return edit.keychord_press(Editor_state, chord, key)
+    return edit.keychord_press(Editor_state, chord, key, scancode, is_repeat)
   else
-    return log_browser.keychord_press(Log_browser_state, chord, key)
+    return log_browser.keychord_press(Log_browser_state, chord, key, scancode, is_repeat)
   end
 end
 
@@ -393,6 +393,6 @@ function source.key_release(key, scancode)
   if Focus == 'edit' then
     return edit.key_release(Editor_state, key, scancode)
   else
-    return log_browser.keychord_press(Log_browser_state, chordkey, scancode)
+    return log_browser.key_release(Log_browser_state, key, scancode)
   end
 end
diff --git a/source_edit.lua b/source_edit.lua
index a8dd682..77487ea 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -143,13 +143,13 @@ end
 
 function edit.put_cursor_on_next_text_line(State)
   local line = State.cursor1.line
-  while line < #State.lines do
+  if State.lines[line].mode == 'text' then return end
+  while line <= #State.lines and State.lines[line].mode ~= 'text' do
     line = line+1
-    if State.lines[line].mode == 'text' then
-      State.cursor1.line = line
-      State.cursor1.pos = 1
-      break
-    end
+  end
+  if line <= #State.lines and State.lines[line].mode == 'text' then
+    State.cursor1.line = line
+    State.cursor1.pos = 1
   end
 end
 
@@ -232,7 +232,7 @@ function edit.quit(State)
   end
 end
 
-function edit.mouse_press(State, x,y, mouse_button)
+function edit.mouse_press(State, x,y, mouse_button, is_touch, presses)
   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))
@@ -279,7 +279,7 @@ function edit.mouse_press(State, x,y, mouse_button)
         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)
+        Drawing.mouse_press(State, line_index, x,y, mouse_button, is_touch, presses)
         return
       end
     end
@@ -292,12 +292,12 @@ function edit.mouse_press(State, x,y, mouse_button)
   State.selection1 = Text.final_text_loc_on_screen(State)
 end
 
-function edit.mouse_release(State, x,y, mouse_button)
+function edit.mouse_release(State, x,y, mouse_button, is_touch, presses)
   if State.search_term then return end
 --?   print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
   State.mouse_down = nil
   if State.lines.current_drawing then
-    Drawing.mouse_release(State, x,y, mouse_button)
+    Drawing.mouse_release(State, x,y, mouse_button, is_touch, presses)
     if Drawing.before then
       record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
       Drawing.before = nil
@@ -383,7 +383,7 @@ function edit.text_input(State, t)
   schedule_save(State)
 end
 
-function edit.keychord_press(State, chord, key)
+function edit.keychord_press(State, chord, key, scancode, is_repeat)
   if State.selection1.line and
       not State.lines.current_drawing and
       -- printable character created using shift key => delete selection
@@ -502,7 +502,7 @@ function edit.keychord_press(State, chord, key)
     local drawing_index, drawing = Drawing.current_drawing(State)
     if drawing_index then
       local before = snapshot(State, drawing_index)
-      Drawing.keychord_press(State, chord)
+      Drawing.keychord_press(State, chord, key, scancode, is_repeat)
       record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
       schedule_save(State)
     end
@@ -535,7 +535,7 @@ function edit.keychord_press(State, chord, key)
     end
     schedule_save(State)
   else
-    Text.keychord_press(State, chord)
+    Text.keychord_press(State, chord, key, scancode, is_repeat)
   end
 end
 
diff --git a/source_text.lua b/source_text.lua
index 284ed00..c281acf 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -223,7 +223,7 @@ function Text.insert_at_cursor(State, t)
 end
 
 -- Don't handle any keys here that would trigger text_input above.
-function Text.keychord_press(State, chord)
+function Text.keychord_press(State, chord, key, scancode, is_repeat)
 --?   print('chord', chord, State.selection1.line, State.selection1.pos)
   --== shortcuts that mutate text
   if chord == 'return' then
diff --git a/source_text_tests.lua b/source_text_tests.lua
index 7ed4caa..12fb9c6 100644
--- a/source_text_tests.lua
+++ b/source_text_tests.lua
@@ -319,7 +319,7 @@ function test_click_on_empty_line()
   check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')
 end
 
-function test_click_below_all_lines()
+function test_click_below_final_line_of_file()
   -- display one line
   App.screen.init{width=50, height=80}
   Editor_state = edit.initialize_test_state()
@@ -331,8 +331,9 @@ function test_click_below_all_lines()
   -- click below first line
   edit.draw(Editor_state)
   edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+50, 1)
-  -- cursor doesn't move
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
+  -- cursor goes to bottom
+  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
+  check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
   -- selection remains empty
   check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')
 end
diff --git a/source_undo.lua b/source_undo.lua
index d91fecd..772e5da 100644
--- a/source_undo.lua
+++ b/source_undo.lua
@@ -84,12 +84,12 @@ end
 -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
 function deepcopy(obj, seen)
   if type(obj) ~= 'table' then return obj end
-  if seen and seen[obj] then return seen[obj] end
-  local s = seen or {}
+  seen = seen or {}
+  if seen[obj] then return seen[obj] end
   local result = setmetatable({}, getmetatable(obj))
-  s[obj] = result
+  seen[obj] = result
   for k,v in pairs(obj) do
-    result[deepcopy(k, s)] = deepcopy(v, s)
+    result[deepcopy(k, seen)] = deepcopy(v, seen)
   end
   return result
 end
diff --git a/text.lua b/text.lua
index da4d2a1..a417fab 100644
--- a/text.lua
+++ b/text.lua
@@ -38,12 +38,10 @@ function Text.draw(State, line_index, y, startpos)
           end
         else
           if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
-            Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
+            Text.pretend_draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
           elseif pos + frag_len == State.cursor1.pos then
-            -- Show cursor at end of line.
-            -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
-            -- It seems useful to see a cursor whether your eye is on the left or right margin.
-            Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
+            -- Keep pretend cursor_x/cursor_y in sync with upstream.
+            Text.pretend_draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
           end
         end
       end
@@ -80,6 +78,11 @@ function Text.draw_cursor(State, x, y)
   State.cursor_y = y+State.line_height
 end
 
+function Text.pretend_draw_cursor(State, x, y)
+  State.cursor_x = x
+  State.cursor_y = y+State.line_height
+end
+
 function Text.populate_screen_line_starting_pos(State, line_index)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
@@ -129,124 +132,13 @@ function Text.text_input(State, t)
       -- Key mutated by the keyboard layout. Continue below.
     end
   end
-  local before = snapshot(State, State.cursor1.line)
---?   print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
-  Text.insert_at_cursor(State, t)
-  if State.cursor_y > App.screen.height - State.line_height then
-    Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
-  end
-  record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-end
-
-function Text.insert_at_cursor(State, t)
-  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.
-function Text.keychord_press(State, chord)
+function Text.keychord_press(State, chord, key, scancode, is_repeat)
 --?   print('chord', chord, State.selection1.line, State.selection1.pos)
-  --== shortcuts that mutate text (must schedule_save)
-  if chord == 'return' then
-    local before_line = State.cursor1.line
-    local before = snapshot(State, before_line)
-    Text.insert_return(State)
-    if State.cursor_y > App.screen.height - State.line_height then
-      Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
-    end
-    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
-    schedule_save(State)
-  elseif chord == 'tab' then
-    local before = snapshot(State, State.cursor1.line)
---?     print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
-    Text.insert_at_cursor(State, '\t')
-    if State.cursor_y > App.screen.height - State.line_height then
-      Text.populate_screen_line_starting_pos(State, State.cursor1.line)
-      Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
---?       print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
-    end
-    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-    schedule_save(State)
-  elseif chord == 'backspace' then
-    if State.selection1.line then
-      Text.delete_selection_and_record_undo_event(State)
-      schedule_save(State)
-      return
-    end
-    local before
-    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)
-      if byte_start then
-        if byte_end then
-          State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
-        else
-          State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
-        end
-        State.cursor1.pos = State.cursor1.pos-1
-      end
-    elseif State.cursor1.line > 1 then
-      before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
-      -- join lines
-      State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
-      State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
-      table.remove(State.lines, State.cursor1.line)
-      table.remove(State.line_cache, State.cursor1.line)
-      State.cursor1.line = State.cursor1.line-1
-    end
-    if State.screen_top1.line > #State.lines then
-      Text.populate_screen_line_starting_pos(State, #State.lines)
-      local line_cache = State.line_cache[#State.line_cache]
-      State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
-    elseif Text.lt1(State.cursor1, State.screen_top1) then
-      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 line caches to avoid memory leaks
-    end
-    Text.clear_screen_line_cache(State, State.cursor1.line)
-    assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
-    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-    schedule_save(State)
-  elseif chord == 'delete' then
-    if State.selection1.line then
-      Text.delete_selection_and_record_undo_event(State)
-      schedule_save(State)
-      return
-    end
-    local before
-    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 <= utf8.len(State.lines[State.cursor1.line].data) then
-      local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
-      local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
-      if byte_start then
-        if byte_end then
-          State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
-        else
-          State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
-        end
-        -- no change to State.cursor1.pos
-      end
-    elseif State.cursor1.line < #State.lines then
-      -- join lines
-      State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
-      table.remove(State.lines, State.cursor1.line+1)
-      table.remove(State.line_cache, State.cursor1.line+1)
-    end
-    Text.clear_screen_line_cache(State, State.cursor1.line)
-    record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
-    schedule_save(State)
   --== shortcuts that move the cursor
-  elseif chord == 'left' then
+  if chord == 'left' then
     Text.left(State)
     State.selection1 = {}
   elseif chord == 'right' then
@@ -330,15 +222,6 @@ function Text.keychord_press(State, chord)
   end
 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, {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)
   State.screen_top1 = Text.previous_screen_top1(State)
   State.cursor1 = deepcopy(State.screen_top1)
diff --git a/text_tests b/text_tests
index 2a31131..85d9de9 100644
--- a/text_tests
+++ b/text_tests
@@ -23,7 +23,7 @@ click on wrapping line rendered from partway at top of screen
 click past end of wrapping line
 click past end of wrapping line containing non ascii
 click past end of word wrapping line
-click below final line does nothing
+click below final line of file
 
 # cursor movement
 move left
diff --git a/text_tests.lua b/text_tests.lua
index 7bf2221..79c38c5 100644
--- a/text_tests.lua
+++ b/text_tests.lua
@@ -15,43 +15,6 @@ function test_initial_state()
   check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')
 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}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def'}
-  Editor_state.screen_top1 = {line=2, pos=1}
-  Editor_state.cursor1 = {line=2, pos=1}
-  Text.redraw_all(Editor_state)
-  -- backspace scrolls up
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(#Editor_state.lines, 1, '#lines')
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
-  check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
-end
-
-function test_insert_first_character()
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{}
-  Text.redraw_all(Editor_state)
-  edit.draw(Editor_state)
-  edit.run_after_text_input(Editor_state, 'a')
-  local y = Editor_state.top
-  App.screen.check(y, 'a', 'screen:1')
-end
-
-function test_press_ctrl()
-  -- press ctrl while the cursor is on text
-  App.screen.init{width=50, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{''}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.run_after_keychord(Editor_state, 'C-m', 'm')
-end
-
 function test_move_left()
   App.screen.init{width=120, height=60}
   Editor_state = edit.initialize_test_state()
@@ -293,7 +256,7 @@ function test_click_on_empty_line()
   check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')
 end
 
-function test_click_below_all_lines()
+function test_click_below_final_line_of_file()
   -- display one line
   App.screen.init{width=50, height=80}
   Editor_state = edit.initialize_test_state()
@@ -305,8 +268,9 @@ function test_click_below_all_lines()
   -- click below first line
   edit.draw(Editor_state)
   edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+50, 1)
-  -- cursor doesn't move
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
+  -- cursor goes to bottom
+  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
+  check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
   -- selection remains empty
   check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')
 end
@@ -574,43 +538,6 @@ function test_cursor_movement_without_shift_resets_selection()
   check_eq(Editor_state.lines[1].data, 'abc', 'data')
 end
 
-function test_edit_deletes_selection()
-  -- display a line of text with some part selected
-  App.screen.init{width=75, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.selection1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  -- press a key
-  edit.run_after_text_input(Editor_state, 'x')
-  -- selected text is deleted and replaced with the key
-  check_eq(Editor_state.lines[1].data, 'xbc', 'check')
-end
-
-function test_edit_with_shift_key_deletes_selection()
-  -- display a line of text with some part selected
-  App.screen.init{width=75, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.selection1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  -- mimic precise keypresses for a capital letter
-  App.fake_key_press('lshift')
-  edit.keychord_press(Editor_state, 'd', 'd')
-  edit.text_input(Editor_state, 'D')
-  edit.key_release(Editor_state, 'd')
-  App.fake_key_release('lshift')
-  -- selected text is deleted and replaced with the key
-  check_nil(Editor_state.selection1.line, 'check')
-  check_eq(Editor_state.lines[1].data, 'Dbc', 'data')
-end
-
 function test_copy_does_not_reset_selection()
   -- display a line of text with a selection
   App.screen.init{width=75, height=80}
@@ -628,154 +555,21 @@ function test_copy_does_not_reset_selection()
   check(Editor_state.selection1.line, 'check')
 end
 
-function test_cut()
-  -- display a line of text with some part selected
-  App.screen.init{width=75, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.selection1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  -- press a key
-  edit.run_after_keychord(Editor_state, 'C-x', 'x')
-  check_eq(App.clipboard, 'a', 'clipboard')
-  -- selected text is deleted
-  check_eq(Editor_state.lines[1].data, 'bc', 'data')
-end
-
-function test_paste_replaces_selection()
-  -- display a line of text with a selection
-  App.screen.init{width=75, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=1}
-  Editor_state.selection1 = {line=1, pos=1}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  -- set clipboard
-  App.clipboard = 'xyz'
-  -- paste selection
-  edit.run_after_keychord(Editor_state, 'C-v', 'v')
-  -- selection is reset since shift key is not pressed
-  -- selection includes the newline, so it's also deleted
-  check_eq(Editor_state.lines[1].data, 'xyzdef', 'check')
-end
-
-function test_deleting_selection_may_scroll()
-  -- display lines 2/3/4
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=3, pos=2}
-  Editor_state.screen_top1 = {line=2, pos=1}
-  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')
-  -- set up a selection starting above the currently displayed page
-  Editor_state.selection1 = {line=1, pos=2}
-  -- delete selection
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  -- page scrolls up
-  check_eq(Editor_state.screen_top1.line, 1, 'check')
-  check_eq(Editor_state.lines[1].data, 'ahi', 'data')
-end
+function test_move_cursor_using_mouse()
 
-function test_edit_wrapping_text()
   App.screen.init{width=50, height=60}
   Editor_state = edit.initialize_test_state()
   Editor_state.lines = load_array{'abc', 'def', 'xyz'}
   Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=4}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  edit.run_after_text_input(Editor_state, 'g')
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'de', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'fg', 'screen:3')
-end
-
-function test_insert_newline()
-  -- display a few lines
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  -- hitting the enter key splits the line
-  edit.run_after_keychord(Editor_state, 'return', 'return')
-  check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'a', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'bc', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'screen:3')
-end
-
-function test_insert_newline_at_start_of_line()
-  -- display a line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc'}
-  Text.redraw_all(Editor_state)
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
-  -- hitting the enter key splits the line
-  edit.run_after_keychord(Editor_state, 'return', 'return')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  check_eq(Editor_state.lines[1].data, '', 'data:1')
-  check_eq(Editor_state.lines[2].data, 'abc', 'data:2')
-end
-
-function test_insert_from_clipboard()
-  -- display a few lines
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  -- paste some text including a newline, check that new line is created
-  App.clipboard = 'xy\nz'
-  edit.run_after_keychord(Editor_state, 'C-v', 'v')
-  check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
+  Editor_state.selection1 = {}
+  edit.draw(Editor_state)  -- populate line_cache.startpos for each line
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'axy', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'zbc', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'screen:3')
+  check_nil(Editor_state.selection1.line, 'selection:line')
+  check_nil(Editor_state.selection1.pos, 'selection:pos')
 end
 
 function test_select_text_using_mouse()
@@ -929,22 +723,6 @@ function test_select_all_text()
   check_eq(Editor_state.cursor1.pos, 8, 'cursor:pos')
 end
 
-function test_cut_without_selection()
-  -- display a few lines
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  Editor_state.selection1 = {}
-  edit.draw(Editor_state)
-  -- try to cut without selecting text
-  edit.run_after_keychord(Editor_state, 'C-x', 'x')
-  -- no crash
-  check_nil(Editor_state.selection1.line, 'check')
-end
-
 function test_pagedown()
   App.screen.init{width=120, height=45}
   Editor_state = edit.initialize_test_state()
@@ -1375,104 +1153,6 @@ function test_pageup_scrolls_up_from_middle_screen_line()
   App.screen.check(y, 'ghi ', 'screen:3')
 end
 
-function test_enter_on_bottom_line_scrolls_down()
-  -- display a few lines with cursor on bottom line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=3, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  -- after hitting the enter key the screen scrolls down
-  edit.run_after_keychord(Editor_state, 'return', 'return')
-  check_eq(Editor_state.screen_top1.line, 2, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 4, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'def', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'g', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'hi', 'screen:3')
-end
-
-function test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()
-  -- display just the bottom line on screen
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=4, pos=2}
-  Editor_state.screen_top1 = {line=4, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'jkl', 'baseline/screen:1')
-  -- after hitting the enter key the screen does not scroll down
-  edit.run_after_keychord(Editor_state, 'return', 'return')
-  check_eq(Editor_state.screen_top1.line, 4, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 5, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'j', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'kl', 'screen:2')
-end
-
-function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()
-  -- display just an empty bottom line on screen
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', ''}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=1}
-  Editor_state.screen_top1 = {line=2, pos=1}
-  edit.draw(Editor_state)
-  -- after hitting the inserting_text key the screen does not scroll down
-  edit.run_after_text_input(Editor_state, 'a')
-  check_eq(Editor_state.screen_top1.line, 2, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
-  local y = Editor_state.top
-  App.screen.check(y, 'a', 'screen:1')
-end
-
-function test_typing_on_bottom_line_scrolls_down()
-  -- display a few lines with cursor on bottom line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=3, pos=4}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi', 'baseline/screen:3')
-  -- after typing something the line wraps and the screen scrolls down
-  edit.run_after_text_input(Editor_state, 'j')
-  edit.run_after_text_input(Editor_state, 'k')
-  edit.run_after_text_input(Editor_state, 'l')
-  check_eq(Editor_state.screen_top1.line, 2, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 3, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 7, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'def', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghij', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'kl', 'screen:3')
-end
-
 function test_left_arrow_scrolls_up_in_wrapped_line()
   -- display lines starting from second screen line of a line
   App.screen.init{width=Editor_state.left+30, height=60}
@@ -1587,293 +1267,6 @@ function test_end_scrolls_down_in_wrapped_line()
   App.screen.check(y, 'jkl', 'screen:3')
 end
 
-function test_position_cursor_on_recently_edited_wrapping_line()
-  -- draw a line wrapping over 2 screen lines
-  App.screen.init{width=100, height=200}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=25}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'abc def ghi ', 'baseline1/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'jkl mno pqr ', 'baseline1/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'xyz', 'baseline1/screen:3')
-  -- add to the line until it's wrapping over 3 screen lines
-  edit.run_after_text_input(Editor_state, 's')
-  edit.run_after_text_input(Editor_state, 't')
-  edit.run_after_text_input(Editor_state, 'u')
-  check_eq(Editor_state.cursor1.pos, 28, 'cursor:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'abc def ghi ', 'baseline2/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'jkl mno pqr ', 'baseline2/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'stu', 'baseline2/screen:3')
-  -- try to move the cursor earlier in the third screen line by clicking the mouse
-  edit.run_after_mouse_release(Editor_state, Editor_state.left+2,Editor_state.top+Editor_state.line_height*2+5, 1)
-  -- cursor should move
-  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 25, 'cursor:pos')
-end
-
-function test_backspace_can_scroll_up()
-  -- display the lines 2/3/4 with the cursor on line 2
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=1}
-  Editor_state.screen_top1 = {line=2, pos=1}
-  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 backspace the screen scrolls up by one line
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
-  check_eq(Editor_state.cursor1.line, 1, 'cursor')
-  y = Editor_state.top
-  App.screen.check(y, 'abcdef', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'ghi', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'jkl', 'screen:3')
-end
-
-function test_backspace_can_scroll_up_screen_line()
-  -- display lines starting from second screen line of a line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=3, pos=5}
-  Editor_state.screen_top1 = {line=3, pos=5}
-  edit.draw(Editor_state)
-  local y = Editor_state.top
-  App.screen.check(y, 'jkl', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'mno', 'baseline/screen:2')
-  -- after hitting backspace the screen scrolls up by one screen line
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  y = Editor_state.top
-  App.screen.check(y, 'ghij', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'kl', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'mno', 'screen:3')
-  check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line')
-  check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')
-  check_eq(Editor_state.cursor1.line, 3, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
-end
-
-function test_backspace_past_line_boundary()
-  -- position cursor at start of a (non-first) line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=1}
-  -- backspace joins with previous line
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'abcdef', 'check')
-end
-
--- some tests for operating over selections created using Shift- chords
--- we're just testing delete_selection, and it works the same for all keys
-
-function test_backspace_over_selection()
-  -- select just one character within a line with cursor before selection
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.selection1 = {line=1, pos=2}
-  -- backspace deletes the selected character, even though it's after the cursor
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'bc', 'data')
-  -- cursor (remains) at start of selection
-  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  -- selection is cleared
-  check_nil(Editor_state.selection1.line, 'selection')
-end
-
-function test_backspace_over_selection_reverse()
-  -- select just one character within a line with cursor after selection
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.selection1 = {line=1, pos=1}
-  -- backspace deletes the selected character
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'bc', 'data')
-  -- cursor moves to start of selection
-  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  -- selection is cleared
-  check_nil(Editor_state.selection1.line, 'selection')
-end
-
-function test_backspace_over_multiple_lines()
-  -- select just one character within a line with cursor after selection
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.selection1 = {line=4, pos=2}
-  -- backspace deletes the region and joins the remaining portions of lines on either side
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'akl', 'data:1')
-  check_eq(Editor_state.lines[2].data, 'mno', 'data:2')
-  -- cursor remains at start of selection
-  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
-  -- selection is cleared
-  check_nil(Editor_state.selection1.line, 'selection')
-end
-
-function test_backspace_to_end_of_line()
-  -- select region from cursor to end of line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=2}
-  Editor_state.selection1 = {line=1, pos=4}
-  -- backspace deletes rest of line without joining to any other line
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'a', 'data:1')
-  check_eq(Editor_state.lines[2].data, 'def', 'data:2')
-  -- cursor remains at start of selection
-  check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
-  -- selection is cleared
-  check_nil(Editor_state.selection1.line, 'selection')
-end
-
-function test_backspace_to_start_of_line()
-  -- select region from cursor to start of line
-  App.screen.init{width=Editor_state.left+30, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=1}
-  Editor_state.selection1 = {line=2, pos=3}
-  -- backspace deletes beginning of line without joining to any other line
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.lines[1].data, 'abc', 'data:1')
-  check_eq(Editor_state.lines[2].data, 'f', 'data:2')
-  -- cursor remains at start of selection
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')
-  -- selection is cleared
-  check_nil(Editor_state.selection1.line, 'selection')
-end
-
-function test_undo_insert_text()
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'def', 'xyz'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=4}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  -- insert a character
-  edit.draw(Editor_state)
-  edit.run_after_text_input(Editor_state, 'g')
-  check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line')
-  check_eq(Editor_state.cursor1.pos, 5, 'baseline/cursor:pos')
-  check_nil(Editor_state.selection1.line, 'baseline/selection:line')
-  check_nil(Editor_state.selection1.pos, 'baseline/selection:pos')
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'defg', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'xyz', 'baseline/screen:3')
-  -- undo
-  edit.run_after_keychord(Editor_state, 'C-z', 'z')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
-  check_nil(Editor_state.selection1.line, 'selection:line')
-  check_nil(Editor_state.selection1.pos, 'selection:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'abc', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'xyz', 'screen:3')
-end
-
-function test_undo_delete_text()
-  App.screen.init{width=120, height=60}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc', 'defg', 'xyz'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=2, pos=5}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  -- delete a character
-  edit.run_after_keychord(Editor_state, 'backspace', 'backspace')
-  check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line')
-  check_eq(Editor_state.cursor1.pos, 4, 'baseline/cursor:pos')
-  check_nil(Editor_state.selection1.line, 'baseline/selection:line')
-  check_nil(Editor_state.selection1.pos, 'baseline/selection:pos')
-  local y = Editor_state.top
-  App.screen.check(y, 'abc', 'baseline/screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'def', 'baseline/screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'xyz', 'baseline/screen:3')
-  -- undo
---?   -- after undo, the backspaced key is selected
-  edit.run_after_keychord(Editor_state, 'C-z', 'z')
-  check_eq(Editor_state.cursor1.line, 2, 'cursor:line')
-  check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')
-  check_nil(Editor_state.selection1.line, 'selection:line')
-  check_nil(Editor_state.selection1.pos, 'selection:pos')
---?   check_eq(Editor_state.selection1.line, 2, 'selection:line')
---?   check_eq(Editor_state.selection1.pos, 4, 'selection:pos')
-  y = Editor_state.top
-  App.screen.check(y, 'abc', 'screen:1')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'defg', 'screen:2')
-  y = y + Editor_state.line_height
-  App.screen.check(y, 'xyz', 'screen:3')
-end
-
-function test_undo_restores_selection()
-  -- display a line of text with some part selected
-  App.screen.init{width=75, height=80}
-  Editor_state = edit.initialize_test_state()
-  Editor_state.lines = load_array{'abc'}
-  Text.redraw_all(Editor_state)
-  Editor_state.cursor1 = {line=1, pos=1}
-  Editor_state.selection1 = {line=1, pos=2}
-  Editor_state.screen_top1 = {line=1, pos=1}
-  edit.draw(Editor_state)
-  -- delete selected text
-  edit.run_after_text_input(Editor_state, 'x')
-  check_eq(Editor_state.lines[1].data, 'xbc', 'baseline')
-  check_nil(Editor_state.selection1.line, 'baseline:selection')
-  -- undo
-  edit.run_after_keychord(Editor_state, 'C-z', 'z')
-  edit.run_after_keychord(Editor_state, 'C-z', 'z')
-  -- selection is restored
-  check_eq(Editor_state.selection1.line, 1, 'line')
-  check_eq(Editor_state.selection1.pos, 2, 'pos')
-end
-
 function test_search()
   App.screen.init{width=120, height=60}
   Editor_state = edit.initialize_test_state()
diff --git a/undo.lua b/undo.lua
index e0cd730..4001fef 100644
--- a/undo.lua
+++ b/undo.lua
@@ -1,92 +1,12 @@
--- undo/redo by managing the sequence of events in the current session
--- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu
-
--- makes a copy of lines on every single keystroke; will be inefficient with really long lines.
--- TODO: highlight stuff inserted by any undo/redo operation
--- TODO: coalesce multiple similar operations
-
-function record_undo_event(State, data)
-  State.history[State.next_history] = data
-  State.next_history = State.next_history+1
-  for i=State.next_history,#State.history do
-    State.history[i] = nil
-  end
-end
-
-function undo_event(State)
-  if State.next_history > 1 then
---?     print('moving to history', State.next_history-1)
-    State.next_history = State.next_history-1
-    local result = State.history[State.next_history]
-    return result
-  end
-end
-
-function redo_event(State)
-  if State.next_history <= #State.history then
---?     print('restoring history', State.next_history+1)
-    local result = State.history[State.next_history]
-    State.next_history = State.next_history+1
-    return result
-  end
-end
-
--- Copy all relevant global state.
--- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
-function snapshot(State, s,e)
-  -- Snapshot everything by default, but subset if requested.
-  assert(s, 'failed to snapshot operation for undo history')
-  if e == nil then
-    e = s
-  end
-  assert(#State.lines > 0, 'failed to snapshot operation for undo history')
-  if s < 1 then s = 1 end
-  if s > #State.lines then s = #State.lines end
-  if e < 1 then e = 1 end
-  if e > #State.lines then e = #State.lines end
-  -- compare with App.initialize_globals
-  local event = {
-    screen_top=deepcopy(State.screen_top1),
-    selection=deepcopy(State.selection1),
-    cursor=deepcopy(State.cursor1),
-    lines={},
-    start_line=s,
-    end_line=e,
-    -- no filename; undo history is cleared when filename changes
-  }
-  for i=s,e do
-    table.insert(event.lines, deepcopy(State.lines[i]))
-  end
-  return event
-end
-
-function patch(lines, from, to)
---?   if #from.lines == 1 and #to.lines == 1 then
---?     assert(from.start_line == from.end_line)
---?     assert(to.start_line == to.end_line)
---?     assert(from.start_line == to.start_line)
---?     lines[from.start_line] = to.lines[1]
---?     return
---?   end
-  assert(from.start_line == to.start_line, 'failed to patch undo operation')
-  for i=from.end_line,from.start_line,-1 do
-    table.remove(lines, i)
-  end
-  assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')
-  for i=1,#to.lines do
-    table.insert(lines, to.start_line+i-1, to.lines[i])
-  end
-end
-
 -- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
 function deepcopy(obj, seen)
   if type(obj) ~= 'table' then return obj end
-  if seen and seen[obj] then return seen[obj] end
-  local s = seen or {}
+  seen = seen or {}
+  if seen[obj] then return seen[obj] end
   local result = setmetatable({}, getmetatable(obj))
-  s[obj] = result
+  seen[obj] = result
   for k,v in pairs(obj) do
-    result[deepcopy(k, s)] = deepcopy(v, s)
+    result[deepcopy(k, seen)] = deepcopy(v, seen)
   end
   return result
 end