about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2022-08-15 10:31:56 -0700
committerKartik K. Agaram <vc@akkartik.com>2022-08-15 11:00:35 -0700
commita655c2113cdc80095402243ce41e1322dea535ab (patch)
tree87fcd7af0242e1791c732c14f47556653cc02f87
parent778f77a4585c538211f79e717bae35fd650eee90 (diff)
downloadview.love-a655c2113cdc80095402243ce41e1322dea535ab.tar.gz
bring back a level of wrapping
Many projects will require the ability to add metadata to lines, so
let's not drop that.
-rw-r--r--edit.lua4
-rw-r--r--file.lua10
-rw-r--r--main_tests.lua6
-rw-r--r--search.lua16
-rw-r--r--select.lua32
-rw-r--r--text.lua86
-rw-r--r--text_tests.lua36
-rw-r--r--undo.lua3
8 files changed, 97 insertions, 96 deletions
diff --git a/edit.lua b/edit.lua
index 1b888a8..2b23769 100644
--- a/edit.lua
+++ b/edit.lua
@@ -18,7 +18,7 @@ edit = {}
 -- run in both tests and a real run
 function edit.initialize_state(top, left, right, font_height, line_height)  -- currently always draws to bottom of screen
   local result = {
-    lines = {''},  -- array of strings
+    lines = {{data=''}},  -- array of strings
 
     -- Lines can be too long to fit on screen, in which case they _wrap_ into
     -- multiple _screen lines_.
@@ -106,7 +106,7 @@ function edit.draw(State)
   if State.cursor_y == -1 then
     State.cursor_y = App.screen.height
   end
---?   print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line]))
+--?   print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line].data))
   if State.search_term then
     Text.draw_search_bar(State)
   end
diff --git a/file.lua b/file.lua
index 84cc74c..94514b2 100644
--- a/file.lua
+++ b/file.lua
@@ -12,11 +12,11 @@ function load_from_file(infile)
     while true do
       local line = infile_next_line()
       if line == nil then break end
-      table.insert(result, line)
+      table.insert(result, {data=line})
     end
   end
   if #result == 0 then
-    table.insert(result, '')
+    table.insert(result, {data=''})
   end
   return result
 end
@@ -27,7 +27,7 @@ function save_to_disk(State)
     error('failed to write to "'..State.filename..'"')
   end
   for _,line in ipairs(State.lines) do
-    outfile:write(line, '\n')
+    outfile:write(line.data, '\n')
   end
   outfile:close()
 end
@@ -40,10 +40,10 @@ function load_array(a)
   while true do
     i,line = next_line(a, i)
     if i == nil then break end
-    table.insert(result, line)
+    table.insert(result, {data=line})
   end
   if #result == 0 then
-    table.insert(result, '')
+    table.insert(result, {data=''})
   end
   return result
 end
diff --git a/main_tests.lua b/main_tests.lua
index 9993d99..b89c634 100644
--- a/main_tests.lua
+++ b/main_tests.lua
@@ -38,9 +38,9 @@ function test_drop_file()
   }
   App.filedropped(fake_dropped_file)
   check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines')
-  check_eq(Editor_state.lines[1], 'abc', 'F - test_drop_file/lines:1')
-  check_eq(Editor_state.lines[2], 'def', 'F - test_drop_file/lines:2')
-  check_eq(Editor_state.lines[3], 'ghi', 'F - test_drop_file/lines:3')
+  check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1')
+  check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2')
+  check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')
 end
 
 function test_drop_file_saves_previous()
diff --git a/search.lua b/search.lua
index ce089d3..1872b9e 100644
--- a/search.lua
+++ b/search.lua
@@ -21,14 +21,14 @@ end
 
 function Text.search_next(State)
   -- search current line from cursor
-  local pos = find(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos)
+  local pos = find(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos)
   if pos then
     State.cursor1.pos = pos
   end
   if pos == nil then
     -- search lines below cursor
     for i=State.cursor1.line+1,#State.lines do
-      pos = find(State.lines[i], State.search_term)
+      pos = find(State.lines[i].data, State.search_term)
       if pos then
         State.cursor1.line = i
         State.cursor1.pos = pos
@@ -39,7 +39,7 @@ function Text.search_next(State)
   if pos == nil then
     -- wrap around
     for i=1,State.cursor1.line-1 do
-      pos = find(State.lines[i], State.search_term)
+      pos = find(State.lines[i].data, State.search_term)
       if pos then
         State.cursor1.line = i
         State.cursor1.pos = pos
@@ -49,7 +49,7 @@ function Text.search_next(State)
   end
   if pos == nil then
     -- search current line until cursor
-    pos = find(State.lines[State.cursor1.line], State.search_term)
+    pos = find(State.lines[State.cursor1.line].data, State.search_term)
     if pos and pos < State.cursor1.pos then
       State.cursor1.pos = pos
     end
@@ -69,14 +69,14 @@ end
 
 function Text.search_previous(State)
   -- search current line before cursor
-  local pos = rfind(State.lines[State.cursor1.line], State.search_term, State.cursor1.pos-1)
+  local pos = rfind(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos-1)
   if pos then
     State.cursor1.pos = pos
   end
   if pos == nil then
     -- search lines above cursor
     for i=State.cursor1.line-1,1,-1 do
-      pos = rfind(State.lines[i], State.search_term)
+      pos = rfind(State.lines[i].data, State.search_term)
       if pos then
         State.cursor1.line = i
         State.cursor1.pos = pos
@@ -87,7 +87,7 @@ function Text.search_previous(State)
   if pos == nil then
     -- wrap around
     for i=#State.lines,State.cursor1.line+1,-1 do
-      pos = rfind(State.lines[i], State.search_term)
+      pos = rfind(State.lines[i].data, State.search_term)
       if pos then
         State.cursor1.line = i
         State.cursor1.pos = pos
@@ -97,7 +97,7 @@ function Text.search_previous(State)
   end
   if pos == nil then
     -- search current line after cursor
-    pos = rfind(State.lines[State.cursor1.line], State.search_term)
+    pos = rfind(State.lines[State.cursor1.line].data, State.search_term)
     if pos and pos > State.cursor1.pos then
       State.cursor1.pos = pos
     end
diff --git a/select.lua b/select.lua
index b89ea33..147d60a 100644
--- a/select.lua
+++ b/select.lua
@@ -53,19 +53,19 @@ end
 -- 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, lo)
-    local hi_offset = Text.offset(line, hi)
-    local pos_offset = Text.offset(line, pos)
+    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:sub(pos_offset, lo_offset-1)
+      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:sub(lo_offset, hi_offset-1)
+    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)
@@ -136,20 +136,20 @@ function Text.delete_selection_without_undo(State)
   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], minp)
-  local max_offset = Text.offset(State.lines[maxl], maxp)
+  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] = State.lines[minl]:sub(1, min_offset-1)..State.lines[minl]:sub(max_offset)
+    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]:sub(max_offset)
+  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] = State.lines[minl]:sub(1, min_offset-1)..rhs
+  State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs
 end
 
 function Text.selection(State)
@@ -165,16 +165,16 @@ function Text.selection(State)
       minp,maxp = maxp,minp
     end
   end
-  local min_offset = Text.offset(State.lines[minl], minp)
-  local max_offset = Text.offset(State.lines[maxl], maxp)
+  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]:sub(min_offset, max_offset-1)
+    return State.lines[minl].data:sub(min_offset, max_offset-1)
   end
   assert(minl < maxl)
-  local result = {State.lines[minl]:sub(min_offset)}
+  local result = {State.lines[minl].data:sub(min_offset)}
   for i=minl+1,maxl-1 do
-    table.insert(result, State.lines[i])
+    table.insert(result, State.lines[i].data)
   end
-  table.insert(result, State.lines[maxl]:sub(1, max_offset-1))
+  table.insert(result, State.lines[maxl].data:sub(1, max_offset-1))
   return table.concat(result, '\n')
 end
diff --git a/text.lua b/text.lua
index 014f77c..d46b6d4 100644
--- a/text.lua
+++ b/text.lua
@@ -50,7 +50,7 @@ function Text.draw(State, line_index, y, startpos)
       if line_index == State.cursor1.line then
         if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
           if State.search_term then
-            if State.lines[State.cursor1.line]:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
+            if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
               local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term))
               App.color(Text_color)
               love.graphics.print(State.search_term, x+lo_px,y)
@@ -92,7 +92,7 @@ function Text.compute_fragments(State, line_index)
   line_cache.fragments = {}
   local x = State.left
   -- try to wrap at word boundaries
-  for frag in line:gmatch('%S*%s*') do
+  for frag in line.data:gmatch('%S*%s*') do
     local frag_text = App.newText(love.graphics.getFont(), frag)
     local frag_width = App.width(frag_text)
 --?     print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
@@ -142,8 +142,8 @@ function Text.textinput(State, t)
 end
 
 function Text.insert_at_cursor(State, t)
-  local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos)
-  State.lines[State.cursor1.line] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line], byte_offset)
+  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
@@ -182,21 +182,21 @@ function Text.keychord_pressed(State, chord)
     local before
     if State.cursor1.pos > 1 then
       before = snapshot(State, State.cursor1.line)
-      local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos-1)
-      local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
+      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] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)..string.sub(State.lines[State.cursor1.line], byte_end)
+          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] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)
+          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])+1
-      State.lines[State.cursor1.line-1] = State.lines[State.cursor1.line-1]..State.lines[State.cursor1.line]
+      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
@@ -222,25 +222,25 @@ function Text.keychord_pressed(State, chord)
       return
     end
     local before
-    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
+    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
       before = snapshot(State, State.cursor1.line)
     else
       before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
     end
-    if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
-      local byte_start = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos)
-      local byte_end = utf8.offset(State.lines[State.cursor1.line], State.cursor1.pos+1)
+    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] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)..string.sub(State.lines[State.cursor1.line], byte_end)
+          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] = string.sub(State.lines[State.cursor1.line], 1, byte_start-1)
+          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] = State.lines[State.cursor1.line]..State.lines[State.cursor1.line+1]
+      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
@@ -333,10 +333,10 @@ function Text.keychord_pressed(State, chord)
 end
 
 function Text.insert_return(State)
-  local byte_offset = Text.offset(State.lines[State.cursor1.line], State.cursor1.pos)
-  table.insert(State.lines, State.cursor1.line+1, string.sub(State.lines[State.cursor1.line], byte_offset))
+  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] = string.sub(State.lines[State.cursor1.line], 1, byte_offset-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
   State.cursor1.pos = 1
@@ -409,8 +409,8 @@ function Text.up(State)
         State.screen_top1.pos = screen_line_starting_pos
 --?         print('pos of top of screen is also '..tostring(State.screen_top1.pos)..' of the same line')
       end
-      local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], screen_line_starting_pos)
-      local s = string.sub(State.lines[State.cursor1.line], screen_line_starting_byte_offset)
+      local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)
+      local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)
       State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
     end
     if State.cursor1.line < State.screen_top1.line then
@@ -426,8 +426,8 @@ function Text.up(State)
       State.screen_top1.pos = new_screen_line_starting_pos
 --?       print('also setting pos of top of screen to '..tostring(State.screen_top1.pos))
     end
-    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos)
-    local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset)
+    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
+    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
     State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
 --?     print('cursor pos is now '..tostring(State.cursor1.pos))
   end
@@ -441,7 +441,7 @@ function Text.down(State)
     if State.cursor1.line < #State.lines then
       local new_cursor_line = State.cursor1.line+1
       State.cursor1.line = new_cursor_line
-      State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line], State.cursor_x, State.left)
+      State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left)
 --?       print(State.cursor1.pos)
     end
     if State.cursor1.line > State.screen_bottom1.line then
@@ -460,8 +460,8 @@ function Text.down(State)
     local screen_line_index, screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line(State)
     new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]
 --?     print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
-    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line], new_screen_line_starting_pos)
-    local s = string.sub(State.lines[State.cursor1.line], new_screen_line_starting_byte_offset)
+    local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
+    local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
     State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
 --?     print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
     if scroll_down then
@@ -481,7 +481,7 @@ function Text.start_of_line(State)
 end
 
 function Text.end_of_line(State)
-  State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1
+  State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
   local _,botpos = Text.pos_at_start_of_cursor_screen_line(State)
   local botline1 = {line=State.cursor1.line, pos=botpos}
   if Text.cursor_past_screen_bottom(State) then
@@ -495,7 +495,7 @@ function Text.word_left(State)
     if State.cursor1.pos == 1 then
       break
     end
-    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%S') then
+    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
       break
     end
     Text.left(State)
@@ -507,7 +507,7 @@ function Text.word_left(State)
       break
     end
     assert(State.cursor1.pos > 1)
-    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos-1, '%s') then
+    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
       break
     end
   end
@@ -516,20 +516,20 @@ end
 function Text.word_right(State)
   -- skip some whitespace
   while true do
-    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then
+    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
       break
     end
-    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%S') then
+    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
       break
     end
     Text.right_without_scroll(State)
   end
   while true do
     Text.right_without_scroll(State)
-    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line]) then
+    if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
       break
     end
-    if Text.match(State.lines[State.cursor1.line], State.cursor1.pos, '%s') then
+    if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
       break
     end
   end
@@ -552,7 +552,7 @@ function Text.left(State)
     State.cursor1.pos = State.cursor1.pos-1
   elseif State.cursor1.line > 1 then
     State.cursor1.line = State.cursor1.line-1
-    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line]) + 1
+    State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
   end
   if Text.lt1(State.cursor1, State.screen_top1) then
     local top2 = Text.to2(State, State.screen_top1)
@@ -569,7 +569,7 @@ function Text.right(State)
 end
 
 function Text.right_without_scroll(State)
-  if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line]) then
+  if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
     State.cursor1.pos = State.cursor1.pos+1
   elseif State.cursor1.line <= #State.lines-1 then
     State.cursor1.line = State.cursor1.line+1
@@ -647,8 +647,8 @@ function Text.to_pos_on_line(State, line_index, mx, my)
   local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
   for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
     local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
-    local screen_line_starting_byte_offset = Text.offset(line, screen_line_starting_pos)
---?     print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line, screen_line_starting_byte_offset))
+    local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
+--?     print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
     local nexty = y + State.line_height
     if my < nexty then
       -- On all wrapped screen lines but the final one, clicks past end of
@@ -658,7 +658,7 @@ function Text.to_pos_on_line(State, line_index, mx, my)
 --?         print('past end of non-final line; return')
         return line_cache.screen_line_starting_pos[screen_line_index+1]-1
       end
-      local s = string.sub(line, screen_line_starting_byte_offset)
+      local s = string.sub(line.data, screen_line_starting_byte_offset)
 --?       print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
       return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1
     end
@@ -671,14 +671,14 @@ function Text.screen_line_width(State, line_index, i)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
   local start_pos = line_cache.screen_line_starting_pos[i]
-  local start_offset = Text.offset(line, start_pos)
+  local start_offset = Text.offset(line.data, start_pos)
   local screen_line
   if i < #line_cache.screen_line_starting_pos then
     local past_end_pos = line_cache.screen_line_starting_pos[i+1]
-    local past_end_offset = Text.offset(line, past_end_pos)
-    screen_line = string.sub(line, start_offset, past_end_offset-1)
+    local past_end_offset = Text.offset(line.data, past_end_pos)
+    screen_line = string.sub(line.data, start_offset, past_end_offset-1)
   else
-    screen_line = string.sub(line, start_pos)
+    screen_line = string.sub(line.data, start_pos)
   end
   local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
   return App.width(screen_line_text)
diff --git a/text_tests.lua b/text_tests.lua
index 5de9ea8..7f9b2e4 100644
--- a/text_tests.lua
+++ b/text_tests.lua
@@ -601,7 +601,7 @@ function test_cursor_movement_without_shift_resets_selection()
   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], 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
+  check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
 end
 
 function test_edit_deletes_selection()
@@ -619,7 +619,7 @@ function test_edit_deletes_selection()
   -- 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], 'xbc', 'F - test_edit_deletes_selection')
+  check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')
 end
 
 function test_edit_with_shift_key_deletes_selection()
@@ -642,7 +642,7 @@ function test_edit_with_shift_key_deletes_selection()
   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], 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
+  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()
@@ -680,7 +680,7 @@ function test_cut()
   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], 'bc', 'F - test_cut/data')
+  check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')
 end
 
 function test_paste_replaces_selection()
@@ -701,7 +701,7 @@ function test_paste_replaces_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], 'xyzdef', 'F - test_paste_replaces_selection')
+  check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection')
 end
 
 function test_deleting_selection_may_scroll()
@@ -727,7 +727,7 @@ function test_deleting_selection_may_scroll()
   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], 'ahi', 'F - test_deleting_selection_may_scroll/data')
+  check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')
 end
 
 function test_edit_wrapping_text()
@@ -793,8 +793,8 @@ function test_insert_newline_at_start_of_line()
   edit.run_after_keychord(Editor_state, 'return')
   check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line')
   check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos')
-  check_eq(Editor_state.lines[1], '', 'F - test_insert_newline_at_start_of_line/data:1')
-  check_eq(Editor_state.lines[2], 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
+  check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1')
+  check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
 end
 
 function test_insert_from_clipboard()
@@ -1758,7 +1758,7 @@ function test_backspace_past_line_boundary()
   Editor_state.cursor1 = {line=2, pos=1}
   -- backspace joins with previous line
   edit.run_after_keychord(Editor_state, 'backspace')
-  check_eq(Editor_state.lines[1], 'abcdef', "F - 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
@@ -1775,7 +1775,7 @@ function test_backspace_over_selection()
   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], 'bc', "F - test_backspace_over_selection/data")
+  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")
@@ -1794,7 +1794,7 @@ function test_backspace_over_selection_reverse()
   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], 'bc', "F - test_backspace_over_selection_reverse/data")
+  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")
@@ -1813,8 +1813,8 @@ function test_backspace_over_multiple_lines()
   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], 'akl', "F - test_backspace_over_multiple_lines/data:1")
-  check_eq(Editor_state.lines[2], 'mno', "F - test_backspace_over_multiple_lines/data:2")
+  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")
@@ -1833,8 +1833,8 @@ function test_backspace_to_end_of_line()
   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], 'a', "F - test_backspace_to_start_of_line/data:1")
-  check_eq(Editor_state.lines[2], 'def', "F - test_backspace_to_start_of_line/data:2")
+  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")
@@ -1853,8 +1853,8 @@ function test_backspace_to_start_of_line()
   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], 'abc', "F - test_backspace_to_start_of_line/data:1")
-  check_eq(Editor_state.lines[2], 'f', "F - test_backspace_to_start_of_line/data:2")
+  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")
@@ -1950,7 +1950,7 @@ function test_undo_restores_selection()
   edit.draw(Editor_state)
   -- delete selected text
   edit.run_after_textinput(Editor_state, 'x')
-  check_eq(Editor_state.lines[1], 'xbc', 'F - test_undo_restores_selection/baseline')
+  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')
diff --git a/undo.lua b/undo.lua
index 9e55fe8..912c949 100644
--- a/undo.lua
+++ b/undo.lua
@@ -57,7 +57,8 @@ function snapshot(State, s,e)
   }
   -- deep copy lines without cached stuff like text fragments
   for i=s,e do
-    table.insert(event.lines, State.lines[i])
+    local line = State.lines[i]
+    table.insert(event.lines, {data=line.data})
   end
   return event
 end