about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--app.lua6
-rw-r--r--conf.lua3
-rw-r--r--drawing.lua6
-rw-r--r--edit.lua21
-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.lua29
-rw-r--r--reference.md3
-rw-r--r--run.lua17
-rw-r--r--search.lua2
-rw-r--r--source.lua27
-rw-r--r--source_edit.lua39
-rw-r--r--source_text.lua6
-rw-r--r--source_text_tests.lua35
-rw-r--r--source_undo.lua8
-rw-r--r--text.lua10
-rw-r--r--text_tests2
-rw-r--r--text_tests.lua35
-rw-r--r--undo.lua8
22 files changed, 193 insertions, 129 deletions
diff --git a/README.md b/README.md
index d0893bb..0887f4a 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,7 @@ modifications break something.
 ## Getting started
 
 Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and
-extremely well-behaved. I'll assume below that you can invoke it using the
-`love` command, but that might vary depending on your OS.
+extremely well-behaved.
 
 To run from the terminal, [pass this directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games),
 optionally with a file path to edit.
diff --git a/app.lua b/app.lua
index 263518c..2375ff9 100644
--- a/app.lua
+++ b/app.lua
@@ -131,7 +131,7 @@ function App.run_tests()
   end
   table.sort(sorted_names)
 --?   App.initialize_for_test() -- debug: run a single test at a time like these 2 lines
---?   test_click_below_all_lines()
+--?   test_search()
   for _,name in ipairs(sorted_names) do
     App.initialize_for_test()
 --?     print('=== '..name)
@@ -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..d9878da
--- /dev/null
+++ b/conf.lua
@@ -0,0 +1,3 @@
+function love.conf(t)
+  t.identity = 'text'
+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 99a71a8..edfcb20 100644
--- a/edit.lua
+++ b/edit.lua
@@ -1,7 +1,7 @@
 -- some constants people might like to tweak
 Text_color = {r=0, g=0, b=0}
 Cursor_color = {r=1, g=0, b=0}
-Highlight_color = {r=0.7, g=0.7, b=0.9}  -- selected text
+Highlight_color = {r=0.7, g=0.7, b=0.9, a=0.4}  -- selected text
 
 Margin_top = 15
 Margin_left = 25
@@ -145,8 +145,7 @@ function edit.quit(State)
   end
 end
 
-function edit.mouse_press(State, x,y, mouse_button)
-  love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
+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))
@@ -188,10 +187,10 @@ function edit.mouse_press(State, x,y, mouse_button)
   State.old_cursor1 = State.cursor1
   State.old_selection1 = State.selection1
   State.mousepress_shift = App.shift_down()
-  State.selection1 = Text.final_text_loc_on_screen(State)
+  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
@@ -214,7 +213,7 @@ function edit.mouse_release(State, x,y, mouse_button)
   end
 
   -- still here? mouse release is below all screen lines
-  State.cursor1 = Text.final_text_loc_on_screen(State)
+  State.cursor1 = Text.final_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
@@ -258,7 +257,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
       -- printable character created using shift key => delete selection
       -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
@@ -284,8 +283,10 @@ function edit.keychord_press(State, chord, key)
       State.screen_top = deepcopy(State.search_backup.screen_top)
       Text.search_next(State)
     elseif chord == 'down' then
-      State.cursor1.pos = State.cursor1.pos+1
-      Text.search_next(State)
+      if #State.search_term > 0 then
+        Text.right(State)
+        Text.search_next(State)
+      end
     elseif chord == 'up' then
       Text.search_previous(State)
     end
@@ -366,7 +367,7 @@ function edit.keychord_press(State, chord, key)
     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 582f05a..83049c2 100644
--- a/main.lua
+++ b/main.lua
@@ -113,16 +113,15 @@ function check_love_version_for_tests()
   end
 end
 
-function App.initialize(arg)
-  love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
+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..'"')
@@ -208,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
@@ -252,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
@@ -294,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
@@ -320,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 307505d..9511726 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()
@@ -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)
@@ -166,14 +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
-  return edit.mouse_press(Editor_state, x,y, mouse_button)
+  love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
+  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)
@@ -186,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/search.lua b/search.lua
index d3a5fea..4aa1f02 100644
--- a/search.lua
+++ b/search.lua
@@ -17,6 +17,7 @@ function Text.draw_search_bar(State)
 end
 
 function Text.search_next(State)
+  if #State.search_term == 0 then return end
   -- search current line from cursor
   local curr_pos = State.cursor1.pos
   local curr_line = State.lines[State.cursor1.line].data
@@ -71,6 +72,7 @@ function Text.search_next(State)
 end
 
 function Text.search_previous(State)
+  if #State.search_term == 0 then return end
   -- search current line before cursor
   local curr_pos = State.cursor1.pos
   local curr_line = State.lines[State.cursor1.line].data
diff --git a/source.lua b/source.lua
index c85517d..bb42440 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()
@@ -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)
@@ -283,14 +283,15 @@ 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)
 --?   print(Editor_state.left, Editor_state.right)
 --?   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
@@ -299,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
 
@@ -341,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
@@ -381,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
 
@@ -392,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 2dca05d..77487ea 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -142,14 +142,13 @@ function edit.cursor_on_text(State)
 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
+  local line = State.cursor1.line
+  if State.lines[line].mode == 'text' then return end
+  while line <= #State.lines and State.lines[line].mode ~= 'text' do
+    line = line+1
+  end
+  if line <= #State.lines and State.lines[line].mode == 'text' then
+    State.cursor1.line = line
     State.cursor1.pos = 1
   end
 end
@@ -233,8 +232,7 @@ function edit.quit(State)
   end
 end
 
-function edit.mouse_press(State, x,y, mouse_button)
-  love.keyboard.setTextInput(true)  -- bring up keyboard on touch screen
+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))
@@ -281,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
@@ -294,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
@@ -385,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
@@ -408,9 +406,14 @@ function edit.keychord_press(State, chord, key)
       local len = utf8.len(State.search_term)
       local byte_offset = Text.offset(State.search_term, len)
       State.search_term = string.sub(State.search_term, 1, byte_offset-1)
-    elseif chord == 'down' then
-      State.cursor1.pos = State.cursor1.pos+1
+      State.cursor = deepcopy(State.search_backup.cursor)
+      State.screen_top = deepcopy(State.search_backup.screen_top)
       Text.search_next(State)
+    elseif chord == 'down' then
+      if #State.search_term > 0 then
+        Text.right(State)
+        Text.search_next(State)
+      end
     elseif chord == 'up' then
       Text.search_previous(State)
     end
@@ -499,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
@@ -532,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 9125cb0..c281acf 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -90,9 +90,9 @@ function Text.screen_line(line, line_cache, i)
   if i >= #line_cache.screen_line_starting_pos then
     return line.data:sub(offset)
   end
-  local endpos = line_cache.screen_line_starting_pos[i+1]-1
+  local endpos = line_cache.screen_line_starting_pos[i+1]
   local end_offset = Text.offset(line.data, endpos)
-  return line.data:sub(offset, end_offset)
+  return line.data:sub(offset, end_offset-1)
 end
 
 function Text.draw_cursor(State, x, y)
@@ -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 11cc823..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
@@ -2073,3 +2074,31 @@ function test_search_wrap_upwards()
   check_eq(Editor_state.cursor1.line, 1, '1/cursor:line')
   check_eq(Editor_state.cursor1.pos, 6, '1/cursor:pos')
 end
+
+function test_search_downwards_from_end_of_line()
+  App.screen.init{width=120, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'ghi'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=4}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  edit.draw(Editor_state)
+  -- search for empty string
+  edit.run_after_keychord(Editor_state, 'C-f', 'f')
+  edit.run_after_keychord(Editor_state, 'down', 'down')
+  -- no crash
+end
+
+function test_search_downwards_from_final_pos_of_line()
+  App.screen.init{width=120, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'ghi'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=3}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  edit.draw(Editor_state)
+  -- search for empty string
+  edit.run_after_keychord(Editor_state, 'C-f', 'f')
+  edit.run_after_keychord(Editor_state, 'down', 'down')
+  -- no crash
+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 82b4ab8..9db8b16 100644
--- a/text.lua
+++ b/text.lua
@@ -65,9 +65,9 @@ function Text.screen_line(line, line_cache, i)
   if i >= #line_cache.screen_line_starting_pos then
     return line.data:sub(offset)
   end
-  local endpos = line_cache.screen_line_starting_pos[i+1]-1
+  local endpos = line_cache.screen_line_starting_pos[i+1]
   local end_offset = Text.offset(line.data, endpos)
-  return line.data:sub(offset, end_offset)
+  return line.data:sub(offset, end_offset-1)
 end
 
 function Text.draw_cursor(State, x, y)
@@ -147,7 +147,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 (must schedule_save)
   if chord == 'return' then
@@ -610,7 +610,7 @@ function Text.pos_at_end_of_screen_line(State, loc1)
   assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
-function Text.final_text_loc_on_screen(State)
+function Text.final_loc_on_screen(State)
   local screen_bottom1 = Text.screen_bottom1(State)
   return {
     line=screen_bottom1.line,
@@ -926,7 +926,7 @@ function Text.tweak_screen_top_and_cursor(State)
     State.cursor1 = deepcopy(State.screen_top1)
   elseif State.cursor1.line >= screen_bottom1.line then
     if Text.cursor_out_of_screen(State) then
-      State.cursor1 = Text.final_text_loc_on_screen(State)
+      State.cursor1 = Text.final_loc_on_screen(State)
     end
   end
 end
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 9b3b60c..962de7d 100644
--- a/text_tests.lua
+++ b/text_tests.lua
@@ -293,7 +293,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 +305,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
@@ -1950,3 +1951,31 @@ function test_search_wrap_upwards()
   check_eq(Editor_state.cursor1.line, 1, '1/cursor:line')
   check_eq(Editor_state.cursor1.pos, 6, '1/cursor:pos')
 end
+
+function test_search_downwards_from_end_of_line()
+  App.screen.init{width=120, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'ghi'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=4}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  edit.draw(Editor_state)
+  -- search for empty string
+  edit.run_after_keychord(Editor_state, 'C-f', 'f')
+  edit.run_after_keychord(Editor_state, 'down', 'down')
+  -- no crash
+end
+
+function test_search_downwards_from_final_pos_of_line()
+  App.screen.init{width=120, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'ghi'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=3}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  edit.draw(Editor_state)
+  -- search for empty string
+  edit.run_after_keychord(Editor_state, 'C-f', 'f')
+  edit.run_after_keychord(Editor_state, 'down', 'down')
+  -- no crash
+end
diff --git a/undo.lua b/undo.lua
index e0cd730..69f7c31 100644
--- a/undo.lua
+++ b/undo.lua
@@ -81,12 +81,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