about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2022-09-06 10:05:20 -0700
committerKartik K. Agaram <vc@akkartik.com>2022-09-06 10:05:20 -0700
commit73fefa7d0961d3831da6c8b2eb7b1b05e3614a69 (patch)
tree281ecb58e135e854cde4b3aa53f5984ce9aa37fb
parent516944b57215db43e20678b7462a46c1beae99ea (diff)
downloadtext.love-73fefa7d0961d3831da6c8b2eb7b1b05e3614a69.tar.gz
support selections in the source editor
I've only tested side A so far, and included a statement of how I want
side B to behave.
-rw-r--r--main.lua2
-rw-r--r--source_edit.lua57
-rw-r--r--source_select.lua183
-rw-r--r--source_text.lua61
-rw-r--r--source_text_tests.lua410
-rw-r--r--text.lua1
6 files changed, 708 insertions, 6 deletions
diff --git a/main.lua b/main.lua
index efb7419..fd0fe45 100644
--- a/main.lua
+++ b/main.lua
@@ -60,7 +60,7 @@ function App.load()
       load_file_from_source_or_save_directory('log_browser.lua')
       load_file_from_source_or_save_directory('source_text.lua')
         load_file_from_source_or_save_directory('search.lua')
-        load_file_from_source_or_save_directory('select.lua')
+        load_file_from_source_or_save_directory('source_select.lua')
         load_file_from_source_or_save_directory('source_undo.lua')
         load_file_from_source_or_save_directory('colorize.lua')
       load_file_from_source_or_save_directory('source_text_tests.lua')
diff --git a/source_edit.lua b/source_edit.lua
index 6676b42..88d5e76 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -76,6 +76,14 @@ function edit.initialize_state(top, left, right, font_height, line_height)  -- c
     cursor1 = {line=1, pos=1, posB=nil},  -- position of cursor
     screen_bottom1 = {line=1, pos=1, posB=nil},  -- position of start of screen line at bottom of screen
 
+    selection1 = {},
+    -- some extra state to compute selection between mouse press and release
+    old_cursor1 = nil,
+    old_selection1 = nil,
+    mousepress_shift = nil,
+    -- when selecting text, avoid recomputing some state on every single frame
+    recent_mouse = {},
+
     -- cursor coordinates in pixels
     cursor_x = 0,
     cursor_y = 0,
@@ -208,9 +216,22 @@ function edit.mouse_pressed(State, x,y, mouse_button)
   for line_index,line in ipairs(State.lines) do
     if line.mode == 'text' then
       if Text.in_line(State, line_index, x,y) then
+        -- delicate dance between cursor, selection and old cursor/selection
+        -- scenarios:
+        --  regular press+release: sets cursor, clears selection
+        --  shift press+release:
+        --    sets selection to old cursor if not set otherwise leaves it untouched
+        --    sets cursor
+        --  press and hold to start a selection: sets selection on press, cursor on release
+        --  press and hold, then press shift: ignore shift
+        --    i.e. mouse_released should never look at shift state
+        State.old_cursor1 = State.cursor1
+        State.old_selection1 = State.selection1
+        State.mousepress_shift = App.shift_down()
         local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
   --?       print(x,y, 'setting cursor:', line_index, pos, posB)
-        State.cursor1 = {line=line_index, pos=pos, posB=posB}
+        State.selection1 = {line=line_index, pos=pos, posB=posB}
+--?         print('selection', State.selection1.line, State.selection1.pos, State.selection1.posB)
         break
       end
     elseif line.mode == 'drawing' then
@@ -236,6 +257,30 @@ function edit.mouse_released(State, x,y, mouse_button)
       record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
       Drawing.before = nil
     end
+  else
+    for line_index,line in ipairs(State.lines) do
+      if line.mode == 'text' then
+        if Text.in_line(State, line_index, x,y) then
+--?           print('reset selection')
+          local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
+          State.cursor1 = {line=line_index, pos=pos, posB=posB}
+--?           print('cursor', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
+          if State.mousepress_shift then
+            if State.old_selection1.line == nil then
+              State.selection1 = State.old_cursor1
+            else
+              State.selection1 = State.old_selection1
+            end
+          end
+          State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
+          if eq(State.cursor1, State.selection1) then
+            State.selection1 = {}
+          end
+          break
+        end
+      end
+    end
+--?     print('selection:', State.selection1.line, State.selection1.pos)
   end
 end
 
@@ -258,6 +303,14 @@ function edit.textinput(State, t)
 end
 
 function edit.keychord_pressed(State, chord, key)
+  if State.selection1.line and
+      not State.lines.current_drawing and
+      -- printable character created using shift key => delete selection
+      -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
+      (not App.shift_down() or utf8.len(key) == 1) and
+      chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) then
+    Text.delete_selection(State, State.left, State.right)
+  end
   if State.search_term then
     if chord == 'escape' then
       State.search_term = nil
@@ -336,6 +389,7 @@ function edit.keychord_pressed(State, chord, key)
       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)
       patch_placeholders(State.line_cache, event.after, event.before)
       -- invalidate various cached bits of lines
@@ -351,6 +405,7 @@ function edit.keychord_pressed(State, chord, key)
       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)
       -- invalidate various cached bits of lines
       State.lines.current_drawing = nil
diff --git a/source_select.lua b/source_select.lua
new file mode 100644
index 0000000..297a7bc
--- /dev/null
+++ b/source_select.lua
@@ -0,0 +1,183 @@
+-- helpers for selecting portions of text
+-- To keep things simple, we'll ignore the B side when selections start on the
+-- A side, and stick to within a single B side selections start in.
+
+-- Return any intersection of the region from State.selection1 to State.cursor1 (or
+-- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and
+-- currently over a drawing) with the region between {line=line_index, pos=apos}
+-- and {line=line_index, pos=bpos}.
+-- apos must be less than bpos. However State.selection1 and State.cursor1 can be in any order.
+-- Result: positions spos,epos between apos,bpos.
+function Text.clip_selection(State, line_index, apos, bpos)
+  if State.selection1.line == nil then return nil,nil end
+  -- min,max = sorted(State.selection1,State.cursor1)
+  local minl,minp = State.selection1.line,State.selection1.pos
+  local maxl,maxp
+  if App.mouse_down(1) then
+    maxl,maxp = Text.mouse_pos(State)
+  else
+    maxl,maxp = State.cursor1.line,State.cursor1.pos
+  end
+  if Text.lt1({line=maxl, pos=maxp},
+              {line=minl, pos=minp}) then
+    minl,maxl = maxl,minl
+    minp,maxp = maxp,minp
+  end
+  -- check if intervals are disjoint
+  if line_index < minl then return nil,nil end
+  if line_index > maxl then return nil,nil end
+  if line_index == minl and bpos <= minp then return nil,nil end
+  if line_index == maxl and apos >= maxp then return nil,nil end
+  -- compare bounds more carefully (start inclusive, end exclusive)
+  local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})
+  local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})
+--?   print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)
+  if a_ge and b_lt then
+    -- fully contained
+    return apos,bpos
+  elseif a_ge then
+    assert(maxl == line_index)
+    return apos,maxp
+  elseif b_lt then
+    assert(minl == line_index)
+    return minp,bpos
+  else
+    assert(minl == maxl and minl == line_index)
+    return minp,maxp
+  end
+end
+
+-- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line
+-- Creates text objects every time, so use this sparingly.
+-- Returns some intermediate computation useful elsewhere.
+function Text.draw_highlight(State, line, x,y, pos, lo,hi)
+  if lo then
+    local lo_offset = Text.offset(line.data, lo)
+    local hi_offset = Text.offset(line.data, hi)
+    local pos_offset = Text.offset(line.data, pos)
+    local lo_px
+    if pos == lo then
+      lo_px = 0
+    else
+      local before = line.data:sub(pos_offset, lo_offset-1)
+      local before_text = App.newText(love.graphics.getFont(), before)
+      lo_px = App.width(before_text)
+    end
+--?     print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px)
+    local s = line.data:sub(lo_offset, hi_offset-1)
+    local text = App.newText(love.graphics.getFont(), s)
+    local text_width = App.width(text)
+    App.color(Highlight_color)
+    love.graphics.rectangle('fill', x+lo_px,y, text_width,State.line_height)
+    App.color(Text_color)
+    return lo_px
+  end
+end
+
+-- inefficient for some reason, so don't do it on every frame
+function Text.mouse_pos(State)
+  local time = love.timer.getTime()
+  if State.recent_mouse.time and State.recent_mouse.time > time-0.1 then
+    return State.recent_mouse.line, State.recent_mouse.pos
+  end
+  State.recent_mouse.time = time
+  local line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())
+  if line then
+    State.recent_mouse.line = line
+    State.recent_mouse.pos = pos
+  end
+  return State.recent_mouse.line, State.recent_mouse.pos
+end
+
+function Text.to_pos(State, x,y)
+  for line_index,line in ipairs(State.lines) do
+    if line.mode == 'text' then
+      if Text.in_line(State, line_index, x,y) then
+        return line_index, Text.to_pos_on_line(State, line_index, x,y)
+      end
+    end
+  end
+end
+
+function Text.cut_selection(State)
+  if State.selection1.line == nil then return end
+  local result = Text.selection(State)
+  Text.delete_selection(State)
+  return result
+end
+
+function Text.delete_selection(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)
+  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)
+  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
+  local min_offset = Text.offset(State.lines[minl].data, minp)
+  local max_offset = Text.offset(State.lines[maxl].data, maxp)
+  if minl == maxl then
+    return State.lines[minl].data:sub(min_offset, max_offset-1)
+  end
+  assert(minl < maxl)
+  local result = {State.lines[minl].data:sub(min_offset)}
+  for i=minl+1,maxl-1 do
+    if State.lines[i].mode == 'text' then
+      table.insert(result, State.lines[i].data)
+    end
+  end
+  table.insert(result, State.lines[maxl].data:sub(1, max_offset-1))
+  return table.concat(result, '\n')
+end
diff --git a/source_text.lua b/source_text.lua
index 733b25a..5959fd5 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -110,6 +110,10 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos)
         screen_line_starting_pos = pos
         x = State.left
       end
+      if State.selection1.line then
+        local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
+        Text.draw_highlight(State, line, x,y, pos, lo,hi)
+      end
       -- Make [[WikiWords]] (single word, all in one screen line) clickable.
       local trimmed_word = rtrim(frag)  -- compute_fragments puts whitespace at the end
       if starts_with(trimmed_word, '[[') and ends_with(trimmed_word, ']]') then
@@ -172,6 +176,10 @@ function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
         screen_line_starting_pos = pos
         x = State.left
       end
+      if State.selection1.line then
+        local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
+        Text.draw_highlight(State, line, x,y, pos, lo,hi)
+      end
       App.screen.draw(frag_text, x,y)
       -- render cursor if necessary
       if State.cursor1.posB and line_index == State.cursor1.line then
@@ -375,12 +383,13 @@ end
 
 -- Don't handle any keys here that would trigger love.textinput above.
 function Text.keychord_pressed(State, chord)
---?   print('chord', chord)
+--?   print('chord', chord, State.selection1.line, State.selection1.pos)
   --== shortcuts that mutate text
   if chord == 'return' then
     local before_line = State.cursor1.line
     local before = snapshot(State, before_line)
     Text.insert_return(State)
+    State.selection1 = {}
     if State.cursor_y > App.screen.height - State.line_height then
       Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
     end
@@ -398,6 +407,11 @@ function Text.keychord_pressed(State, chord)
     schedule_save(State)
     record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
   elseif chord == 'backspace' then
+    if State.selection1.line then
+      Text.delete_selection(State, State.left, State.right)
+      schedule_save(State)
+      return
+    end
     local before
     if State.cursor1.pos and State.cursor1.pos > 1 then
       before = snapshot(State, State.cursor1.line)
@@ -456,6 +470,11 @@ function Text.keychord_pressed(State, chord)
     schedule_save(State)
     record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
   elseif chord == 'delete' then
+    if State.selection1.line then
+      Text.delete_selection(State, State.left, State.right)
+      schedule_save(State)
+      return
+    end
     local before
     if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
       before = snapshot(State, State.cursor1.line)
@@ -504,44 +523,84 @@ function Text.keychord_pressed(State, chord)
   --== shortcuts that move the cursor
   elseif chord == 'left' then
     Text.left(State)
+    State.selection1 = {}
   elseif chord == 'right' then
     Text.right(State)
+    State.selection1 = {}
   elseif chord == 'S-left' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.left(State)
   elseif chord == 'S-right' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.right(State)
   -- C- hotkeys reserved for drawings, so we'll use M-
   elseif chord == 'M-left' then
     Text.word_left(State)
+    State.selection1 = {}
   elseif chord == 'M-right' then
     Text.word_right(State)
+    State.selection1 = {}
   elseif chord == 'M-S-left' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.word_left(State)
   elseif chord == 'M-S-right' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.word_right(State)
   elseif chord == 'home' then
     Text.start_of_line(State)
+    State.selection1 = {}
   elseif chord == 'end' then
     Text.end_of_line(State)
+    State.selection1 = {}
   elseif chord == 'S-home' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.start_of_line(State)
   elseif chord == 'S-end' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.end_of_line(State)
   elseif chord == 'up' then
     Text.up(State)
+    State.selection1 = {}
   elseif chord == 'down' then
     Text.down(State)
+    State.selection1 = {}
   elseif chord == 'S-up' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.up(State)
   elseif chord == 'S-down' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.down(State)
   elseif chord == 'pageup' then
     Text.pageup(State)
+    State.selection1 = {}
   elseif chord == 'pagedown' then
     Text.pagedown(State)
+    State.selection1 = {}
   elseif chord == 'S-pageup' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.pageup(State)
   elseif chord == 'S-pagedown' then
+    if State.selection1.line == nil then
+      State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}
+    end
     Text.pagedown(State)
   end
 end
diff --git a/source_text_tests.lua b/source_text_tests.lua
index 89ad1ce..5cd0f02 100644
--- a/source_text_tests.lua
+++ b/source_text_tests.lua
@@ -282,6 +282,7 @@ function test_click_with_mouse()
   edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   -- cursor moves
   check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')
+  check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse/selection is empty to avoid perturbing future edits')
 end
 
 function test_click_with_mouse_to_left_of_line()
@@ -300,6 +301,7 @@ function test_click_with_mouse_to_left_of_line()
   -- cursor moves to start of line
   check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:line')
   check_eq(Editor_state.cursor1.pos, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_to_left_of_line/selection is empty to avoid perturbing future edits')
 end
 
 function test_click_with_mouse_takes_margins_into_account()
@@ -319,6 +321,7 @@ function test_click_with_mouse_takes_margins_into_account()
   -- cursor moves
   check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_takes_margins_into_account/cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_takes_margins_into_account/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_takes_margins_into_account/selection is empty to avoid perturbing future edits')
 end
 
 function test_click_with_mouse_on_empty_line()
@@ -408,6 +411,7 @@ function test_click_with_mouse_on_wrapping_line()
   -- cursor moves
   check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line/cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_on_wrapping_line/selection is empty to avoid perturbing future edits')
 end
 
 function test_click_with_mouse_on_wrapping_line_takes_margins_into_account()
@@ -427,6 +431,7 @@ function test_click_with_mouse_on_wrapping_line_takes_margins_into_account()
   -- cursor moves
   check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/selection is empty to avoid perturbing future edits')
 end
 
 function test_draw_text_wrapping_within_word()
@@ -585,6 +590,174 @@ function test_click_past_end_of_word_wrapping_line()
   check_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')
 end
 
+function test_select_text()
+  io.write('\ntest_select_text')
+  -- display a line of text
+  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=1, pos=1}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- select a letter
+  App.fake_key_press('lshift')
+  edit.run_after_keychord(Editor_state, 'S-right')
+  App.fake_key_release('lshift')
+  edit.key_released(Editor_state, 'lshift')
+  -- selection persists even after shift is released
+  check_eq(Editor_state.selection1.line, 1, 'F - test_select_text/selection:line')
+  check_eq(Editor_state.selection1.pos, 1, 'F - test_select_text/selection:pos')
+  check_eq(Editor_state.cursor1.line, 1, 'F - test_select_text/cursor:line')
+  check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text/cursor:pos')
+end
+
+function test_cursor_movement_without_shift_resets_selection()
+  io.write('\ntest_cursor_movement_without_shift_resets_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- press an arrow key without shift
+  edit.run_after_keychord(Editor_state, 'right')
+  -- no change to data, selection is reset
+  check_nil(Editor_state.selection1.line, 'F - test_cursor_movement_without_shift_resets_selection')
+  check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
+end
+
+function test_edit_deletes_selection()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- press a key
+  edit.run_after_textinput(Editor_state, 'x')
+  -- selected text is deleted and replaced with the key
+  check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')
+end
+
+function test_edit_with_shift_key_deletes_selection()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- mimic precise keypresses for a capital letter
+  App.fake_key_press('lshift')
+  edit.keychord_pressed(Editor_state, 'd', 'd')
+  edit.textinput(Editor_state, 'D')
+  edit.key_released(Editor_state, 'd')
+  App.fake_key_release('lshift')
+  -- selected text is deleted and replaced with the key
+  check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection')
+  check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
+end
+
+function test_copy_does_not_reset_selection()
+  io.write('\ntest_copy_does_not_reset_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'}
+  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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- copy selection
+  edit.run_after_keychord(Editor_state, 'C-c')
+  check_eq(App.clipboard, 'a', 'F - test_copy_does_not_reset_selection/clipboard')
+  -- selection is reset since shift key is not pressed
+  check(Editor_state.selection1.line, 'F - test_copy_does_not_reset_selection')
+end
+
+function test_cut()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- press a key
+  edit.run_after_keychord(Editor_state, 'C-x')
+  check_eq(App.clipboard, 'a', 'F - test_cut/clipboard')
+  -- selected text is deleted
+  check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')
+end
+
+function test_paste_replaces_selection()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- set clipboard
+  App.clipboard = 'xyz'
+  -- paste selection
+  edit.run_after_keychord(Editor_state, 'C-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', 'F - test_paste_replaces_selection')
+end
+
+function test_deleting_selection_may_scroll()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  local y = Editor_state.top
+  App.screen.check(y, 'def', 'F - test_deleting_selection_may_scroll/baseline/screen:1')
+  y = y + Editor_state.line_height
+  App.screen.check(y, 'ghi', 'F - test_deleting_selection_may_scroll/baseline/screen:2')
+  y = y + Editor_state.line_height
+  App.screen.check(y, 'jkl', 'F - test_deleting_selection_may_scroll/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')
+  -- page scrolls up
+  check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll')
+  check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')
+end
+
 function test_edit_wrapping_text()
   io.write('\ntest_edit_wrapping_text')
   App.screen.init{width=50, height=60}
@@ -692,10 +865,108 @@ function test_move_cursor_using_mouse()
   Editor_state.cursor1 = {line=1, pos=1}
   Editor_state.screen_top1 = {line=1, pos=1}
   Editor_state.screen_bottom1 = {}
+  Editor_state.selection1 = {}
   edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
-  edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
   check_eq(Editor_state.cursor1.line, 1, 'F - test_move_cursor_using_mouse/cursor:line')
   check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_cursor_using_mouse/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_move_cursor_using_mouse/selection:line')
+  check_nil(Editor_state.selection1.pos, 'F - test_move_cursor_using_mouse/selection:pos')
+end
+
+function test_select_text_using_mouse()
+  io.write('\ntest_select_text_using_mouse')
+  App.screen.init{width=50, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'xyz'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=1}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  Editor_state.screen_bottom1 = {}
+  Editor_state.selection1 = {}
+  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  -- press and hold on first location
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  -- drag and release somewhere else
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)
+  check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_using_mouse/selection:line')
+  check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_using_mouse/selection:pos')
+  check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_using_mouse/cursor:line')
+  check_eq(Editor_state.cursor1.pos, 4, 'F - test_select_text_using_mouse/cursor:pos')
+end
+
+function test_select_text_using_mouse_and_shift()
+  io.write('\ntest_select_text_using_mouse_and_shift')
+  App.screen.init{width=50, height=60}
+  Editor_state = edit.initialize_test_state()
+  Editor_state.lines = load_array{'abc', 'def', 'xyz'}
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=1}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  Editor_state.screen_bottom1 = {}
+  Editor_state.selection1 = {}
+  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  -- click on first location
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  -- hold down shift and click somewhere else
+  App.fake_key_press('lshift')
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)
+  App.fake_key_release('lshift')
+  check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_using_mouse_and_shift/selection:line')
+  check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_using_mouse_and_shift/selection:pos')
+  check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_using_mouse_and_shift/cursor:line')
+  check_eq(Editor_state.cursor1.pos, 4, 'F - test_select_text_using_mouse_and_shift/cursor:pos')
+end
+
+function test_select_text_repeatedly_using_mouse_and_shift()
+  io.write('\ntest_select_text_repeatedly_using_mouse_and_shift')
+  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)
+  Text.redraw_all(Editor_state)
+  Editor_state.cursor1 = {line=1, pos=1}
+  Editor_state.screen_top1 = {line=1, pos=1}
+  Editor_state.screen_bottom1 = {}
+  Editor_state.selection1 = {}
+  edit.draw(Editor_state)  -- populate line_cache.starty for each line Editor_state.line_cache
+  -- click on first location
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
+  -- hold down shift and click on a second location
+  App.fake_key_press('lshift')
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)
+  -- hold down shift and click at a third location
+  App.fake_key_press('lshift')
+  edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height+5, 1)
+  App.fake_key_release('lshift')
+  -- selection is between first and third location. forget the second location, not the first.
+  check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_repeatedly_using_mouse_and_shift/selection:line')
+  check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/selection:pos')
+  check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:line')
+  check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:pos')
+end
+
+function test_cut_without_selection()
+  io.write('\ntest_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.screen_bottom1 = {}
+  Editor_state.selection1 = {}
+  edit.draw(Editor_state)
+  -- try to cut without selecting text
+  edit.run_after_keychord(Editor_state, 'C-x')
+  -- no crash
+  check_nil(Editor_state.selection1.line, 'F - test_cut_without_selection')
 end
 
 function test_pagedown()
@@ -1440,7 +1711,7 @@ function test_position_cursor_on_recently_edited_wrapping_line()
   y = y + Editor_state.line_height
   App.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3')
   -- try to move the cursor earlier in the third screen line by clicking the mouse
-  edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)
+  edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)
   -- cursor should move
   check_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line')
   check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')
@@ -1517,6 +1788,107 @@ function test_backspace_past_line_boundary()
   check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")
 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()
+  io.write('\ntest_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')
+  check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data")
+  -- cursor (remains) at start of selection
+  check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection/cursor:line")
+  check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos")
+  -- selection is cleared
+  check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection/selection")
+end
+
+function test_backspace_over_selection_reverse()
+  io.write('\ntest_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')
+  check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data")
+  -- cursor moves to start of selection
+  check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection_reverse/cursor:line")
+  check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection_reverse/cursor:pos")
+  -- selection is cleared
+  check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection_reverse/selection")
+end
+
+function test_backspace_over_multiple_lines()
+  io.write('\ntest_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')
+  check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1")
+  check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2")
+  -- cursor remains at start of selection
+  check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_multiple_lines/cursor:line")
+  check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_over_multiple_lines/cursor:pos")
+  -- selection is cleared
+  check_nil(Editor_state.selection1.line, "F - test_backspace_over_multiple_lines/selection")
+end
+
+function test_backspace_to_end_of_line()
+  io.write('\ntest_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')
+  check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1")
+  check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2")
+  -- cursor remains at start of selection
+  check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_to_start_of_line/cursor:line")
+  check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_to_start_of_line/cursor:pos")
+  -- selection is cleared
+  check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")
+end
+
+function test_backspace_to_start_of_line()
+  io.write('\ntest_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')
+  check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1")
+  check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2")
+  -- cursor remains at start of selection
+  check_eq(Editor_state.cursor1.line, 2, "F - test_backspace_to_start_of_line/cursor:line")
+  check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_to_start_of_line/cursor:pos")
+  -- selection is cleared
+  check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")
+end
+
 function test_undo_insert_text()
   io.write('\ntest_undo_insert_text')
   App.screen.init{width=120, height=60}
@@ -1531,6 +1903,8 @@ function test_undo_insert_text()
   edit.run_after_textinput(Editor_state, 'g')
   check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line')
   check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/baseline/selection:line')
+  check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/baseline/selection:pos')
   local y = Editor_state.top
   App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')
   y = y + Editor_state.line_height
@@ -1541,6 +1915,8 @@ function test_undo_insert_text()
   edit.run_after_keychord(Editor_state, 'C-z')
   check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/cursor:line')
   check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/selection:line')
+  check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/selection:pos')
   y = Editor_state.top
   App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')
   y = y + Editor_state.line_height
@@ -1562,6 +1938,8 @@ function test_undo_delete_text()
   edit.run_after_keychord(Editor_state, 'backspace')
   check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line')
   check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/baseline/selection:line')
+  check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/baseline/selection:pos')
   local y = Editor_state.top
   App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')
   y = y + Editor_state.line_height
@@ -1573,6 +1951,10 @@ function test_undo_delete_text()
   edit.run_after_keychord(Editor_state, 'C-z')
   check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/cursor:line')
   check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos')
+  check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/selection:line')
+  check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/selection:pos')
+--?   check_eq(Editor_state.selection1.line, 2, 'F - test_undo_delete_text/selection:line')
+--?   check_eq(Editor_state.selection1.pos, 4, 'F - test_undo_delete_text/selection:pos')
   y = Editor_state.top
   App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')
   y = y + Editor_state.line_height
@@ -1581,6 +1963,30 @@ function test_undo_delete_text()
   App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
 end
 
+function test_undo_restores_selection()
+  io.write('\ntest_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}
+  Editor_state.screen_bottom1 = {}
+  edit.draw(Editor_state)
+  -- delete selected text
+  edit.run_after_textinput(Editor_state, 'x')
+  check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline')
+  check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection')
+  -- undo
+  edit.run_after_keychord(Editor_state, 'C-z')
+  edit.run_after_keychord(Editor_state, 'C-z')
+  -- selection is restored
+  check_eq(Editor_state.selection1.line, 1, 'F - test_undo_restores_selection/line')
+  check_eq(Editor_state.selection1.pos, 2, 'F - test_undo_restores_selection/pos')
+end
+
 function test_search()
   io.write('\ntest_search')
   App.screen.init{width=120, height=60}
diff --git a/text.lua b/text.lua
index 6be260d..9720bfa 100644
--- a/text.lua
+++ b/text.lua
@@ -931,7 +931,6 @@ end
 
 -- resize helper
 function Text.tweak_screen_top_and_cursor(State)
---?   print('a', State.selection1.line)
   if State.screen_top1.pos == 1 then return end
   Text.populate_screen_line_starting_pos(State, State.screen_top1.line)
   local line = State.lines[State.screen_top1.line]