diff --git a/source.lua b/source.lua index d0836cb..f224459 100644 --- a/source.lua +++ b/source.lua @@ -75,7 +75,7 @@ function source.initialize() love.window.setTitle('lines.love - source') end --- environment for a mutable file of bifolded text +-- environment for a mutable file -- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up. function source.initialize_edit_side() load_from_disk(Editor_state) @@ -89,17 +89,12 @@ function source.initialize_edit_side() end edit.check_locs(Editor_state) - -- We currently start out with side B collapsed. - -- Other options: - -- * save all expanded state by line - -- * expand all if any location is in side B if Editor_state.cursor1.line > #Editor_state.lines then Editor_state.cursor1 = {line=1, pos=1} end if Editor_state.screen_top1.line > #Editor_state.lines then Editor_state.screen_top1 = {line=1, pos=1} end - edit.eradicate_locations_after_the_fold(Editor_state) if rawget(_G, 'jit') then jit.off() @@ -253,13 +248,6 @@ end function source.quit() edit.quit(Editor_state) log_browser.quit(Log_browser_state) - -- convert any bifold files here -end - -function source.convert_bifold_text(infilename, outfilename) - local contents = love.filesystem.read(infilename) - contents = contents:gsub('\u{1e}', ';') - love.filesystem.write(outfilename, contents) end function source.settings() diff --git a/source_edit.lua b/source_edit.lua index a296c53..964f6ff 100644 --- a/source_edit.lua +++ b/source_edit.lua @@ -10,8 +10,6 @@ Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings Help_color = {r=0, g=0.5, b=0} Help_background_color = {r=0, g=0.5, b=0, a=0.1} -Fold_color = {r=0, g=0.6, b=0} -Fold_background_color = {r=0, g=0.7, b=0} Margin_top = 15 Margin_left = 25 @@ -28,12 +26,10 @@ 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 = { - -- a line is either bifold text or a drawing - -- a line of bifold text consists of an A side and an optional B side + -- a line is either text or a drawing + -- a text is a table with: -- mode = 'text', -- string data, - -- string dataB, - -- expanded: whether to show B side -- a drawing is a table with: -- mode = 'drawing' -- a (y) coord in pixels (updated while painting screen), @@ -50,7 +46,7 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide -- The field names are carefully chosen so that switching modes in midstream -- remembers previously entered points where that makes sense. - lines = {{mode='text', data='', dataB=nil, expanded=nil}}, -- array of lines + lines = {{mode='text', data=''}}, -- array of lines -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -65,16 +61,15 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c -- Given wrapping, any potential location for the text cursor can be described in two ways: -- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units) -- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line. - -- Positions (and screen line indexes) can be in either the A or the B side. -- -- Most of the time we'll only persist positions in schema 1, translating to -- schema 2 when that's convenient. -- -- Make sure these coordinates are never aliased, so that changing one causes -- action at a distance. - screen_top1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at top of screen - 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 + screen_top1 = {line=1, pos=1}, -- position of start of screen line at top of screen + cursor1 = {line=1, pos=1}, -- position of cursor + screen_bottom1 = {line=1, pos=1}, -- position of start of screen line at bottom of screen selection1 = {}, -- some extra state to compute selection between mouse press and release @@ -154,7 +149,7 @@ function edit.draw(State, hide_cursor) assert(false) end if not Text.le1(State.screen_top1, State.cursor1) then - print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB) + print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos) assert(false) end State.cursor_x = nil @@ -163,18 +158,14 @@ function edit.draw(State, hide_cursor) --? print('== draw') for line_index = State.screen_top1.line,#State.lines do local line = State.lines[line_index] ---? print('draw:', y, line_index, line, line.mode) +--? print('draw:', y, line_index, line) if y + State.line_height > App.screen.height then break end - State.screen_bottom1 = {line=line_index, pos=nil, posB=nil} + State.screen_bottom1 = {line=line_index, pos=nil} if line.mode == 'text' then ---? print('text.draw', y, line_index) - local startpos, startposB = 1, nil +--? print('text.draw', y, line_index) + local startpos = 1 if line_index == State.screen_top1.line then - if State.screen_top1.pos then - startpos = State.screen_top1.pos - else - startpos, startposB = nil, State.screen_top1.posB - end + startpos = State.screen_top1.pos end if line.data == '' then -- button to insert new drawing @@ -192,7 +183,7 @@ function edit.draw(State, hide_cursor) end, }) end - y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB, hide_cursor) + y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos, hide_cursor) y = y + State.line_height --? print('=> y', y) elseif line.mode == 'drawing' then @@ -255,10 +246,11 @@ 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() - local pos,posB = Text.to_pos_on_line(State, line_index, x, y) - --? print(x,y, 'setting cursor:', line_index, pos, posB) - State.selection1 = {line=line_index, pos=pos, posB=posB} ---? print('selection', State.selection1.line, State.selection1.pos, State.selection1.posB) + State.selection1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('selection', State.selection1.line, State.selection1.pos) break end elseif line.mode == 'drawing' then @@ -289,9 +281,11 @@ function edit.mouse_release(State, x,y, mouse_button) 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) + State.cursor1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('cursor', State.cursor1.line, State.cursor1.pos) if State.mousepress_shift then if State.old_selection1.line == nil then State.selection1 = State.old_cursor1 @@ -360,11 +354,7 @@ function edit.keychord_press(State, chord, key) State.search_term = string.sub(State.search_term, 1, byte_offset-1) State.search_text = nil elseif chord == 'down' then - if State.cursor1.pos then - State.cursor1.pos = State.cursor1.pos+1 - else - State.cursor1.posB = State.cursor1.posB+1 - end + State.cursor1.pos = State.cursor1.pos+1 Text.search_next(State) elseif chord == 'up' then Text.search_previous(State) @@ -373,35 +363,10 @@ function edit.keychord_press(State, chord, key) elseif chord == 'C-f' then State.search_term = '' State.search_backup = { - cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}, - screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}, + cursor={line=State.cursor1.line, pos=State.cursor1.pos}, + screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos}, } assert(State.search_text == nil) - -- bifold text - elseif chord == 'M-b' then - State.expanded = not State.expanded - Text.redraw_all(State) - if not State.expanded then - for _,line in ipairs(State.lines) do - line.expanded = nil - end - edit.eradicate_locations_after_the_fold(State) - end - elseif chord == 'M-d' then - if State.cursor1.posB == nil then - local before = snapshot(State, State.cursor1.line) - if State.lines[State.cursor1.line].dataB == nil then - State.lines[State.cursor1.line].dataB = '' - end - State.lines[State.cursor1.line].expanded = true - State.cursor1.pos = nil - State.cursor1.posB = 1 - if Text.cursor_out_of_screen(State) then - Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) - end - schedule_save(State) - record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) - end -- zoom elseif chord == 'C-=' then edit.update_font_settings(State, State.font_height+2) @@ -449,7 +414,7 @@ function edit.keychord_press(State, chord, key) -- clipboard elseif chord == 'C-a' then State.selection1 = {line=1, pos=1} - State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1, posB=nil} + State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1} elseif chord == 'C-c' then local s = Text.selection(State) if s then @@ -524,20 +489,6 @@ function edit.keychord_press(State, chord, key) end end -function edit.eradicate_locations_after_the_fold(State) - -- eradicate side B from any locations we track - if State.cursor1.posB then - State.cursor1.posB = nil - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) - State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1) - end - if State.screen_top1.posB then - State.screen_top1.posB = nil - State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data) - State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1) - end -end - function edit.key_release(State, key, scancode) end diff --git a/source_text.lua b/source_text.lua index 1a43842..4cf3edd 100644 --- a/source_text.lua +++ b/source_text.lua @@ -1,90 +1,16 @@ -- text editor, particularly text drawing, horizontal wrap, vertical scrolling Text = {} -AB_padding = 20 -- space in pixels between A side and B side -- draw a line starting from startpos to screen at y between State.left and State.right --- return the final y, and pos,posB of start of final screen line drawn -function Text.draw(State, line_index, y, startpos, startposB, hide_cursor) +-- return the final y, and position of start of final screen line drawn +function Text.draw(State, line_index, y, startpos, hide_cursor) local line = State.lines[line_index] local line_cache = State.line_cache[line_index] line_cache.starty = y line_cache.startpos = startpos - line_cache.startposB = startposB - -- draw A side - local overflows_screen, x, pos, screen_line_starting_pos - if startpos then - overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos) - if overflows_screen then - return y, screen_line_starting_pos - end - if Focus == 'edit' and State.cursor1.pos then - if not hide_cursor and not State.search_term then - if line_index == State.cursor1.line and State.cursor1.pos == pos then - Text.draw_cursor(State, x, y) - end - end - end - else - x = State.left - end - -- check for B side ---? if line_index == 8 then print('checking for B side') end - if line.dataB == nil then - assert(y) - assert(screen_line_starting_pos) ---? if line_index == 8 then print('return 1') end - return y, screen_line_starting_pos - end - if not State.expanded and not line.expanded then - assert(y) - assert(screen_line_starting_pos) ---? if line_index == 8 then print('return 2') end - button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1}, - icon = function(button_params) - App.color(Fold_background_color) - love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2) - end, - onpress1 = function() - line.expanded = true - end, - }) - return y, screen_line_starting_pos - end - -- draw B side ---? if line_index == 8 then print('drawing B side') end - App.color(Fold_color) - if startposB then - overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB) - else - overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1) - end - if overflows_screen then - return y, nil, screen_line_starting_pos - end ---? if line_index == 8 then print('a') end - if Focus == 'edit' and State.cursor1.posB then ---? if line_index == 8 then print('b') end - if not hide_cursor and not State.search_term then ---? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end - if line_index == State.cursor1.line and State.cursor1.posB == pos then - Text.draw_cursor(State, x, y) - end - end - end - return y, nil, screen_line_starting_pos -end - --- Given an array of fragments, draw the subset starting from pos to screen --- starting from (x,y). --- Return: --- - whether we got to bottom of screen before end of line --- - the final (x,y) --- - the final pos --- - starting pos of the final screen line drawn -function Text.draw_wrapping_line(State, line_index, x,y, startpos) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] ---? print('== line', line_index, '^'..line.data..'$') + -- wrap long lines + local x = State.left + local pos = 1 local screen_line_starting_pos = startpos Text.compute_fragments(State, line_index) local pos = 1 @@ -105,7 +31,7 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos) assert(x > State.left) -- no overfull lines y = y + State.line_height if y + State.line_height > App.screen.height then - return --[[screen filled]] true, x,y, pos, screen_line_starting_pos + return y, screen_line_starting_pos end screen_line_starting_pos = pos x = State.left @@ -130,7 +56,7 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos) end App.screen.draw(frag_text, x,y) -- render cursor if necessary - if State.cursor1.pos and line_index == State.cursor1.line then + 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].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then @@ -148,59 +74,12 @@ function Text.draw_wrapping_line(State, line_index, x,y, startpos) end pos = pos + frag_len end - return false, x,y, pos, screen_line_starting_pos -end - -function Text.draw_wrapping_lineB(State, line_index, x,y, startpos) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - local screen_line_starting_pos = startpos - Text.compute_fragmentsB(State, line_index, x) - local pos = 1 - for _, f in ipairs(line_cache.fragmentsB) do - local frag, frag_text = f.data, f.text - local frag_len = utf8.len(frag) ---? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y) - if pos < startpos then - -- render nothing ---? print('skipping', frag) - else - -- render fragment - local frag_width = App.width(frag_text) - if x + frag_width > State.right then - assert(x > State.left) -- no overfull lines - y = y + State.line_height - if y + State.line_height > App.screen.height then - return --[[screen filled]] true, x,y, pos, screen_line_starting_pos - end - 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 - if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then - if State.search_term then - if State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term then - local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)) - App.color(Fold_color) - love.graphics.print(State.search_term, x+lo_px,y) - end - elseif Focus == 'edit' then - Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y) - App.color(Fold_color) - end - end - end - x = x + frag_width + if Focus == 'edit' and not hide_cursor and State.search_term == nil then + if line_index == State.cursor1.line and State.cursor1.pos == pos then + Text.draw_cursor(State, x, y) end - pos = pos + frag_len end - return false, x,y, pos, screen_line_starting_pos + return y, screen_line_starting_pos end function Text.draw_cursor(State, x, y) @@ -285,74 +164,6 @@ function Text.compute_fragments(State, line_index) end end -function Text.populate_screen_line_starting_posB(State, line_index, x) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - if line_cache.screen_line_starting_posB then - return - end - -- duplicate some logic from Text.draw - Text.compute_fragmentsB(State, line_index, x) - line_cache.screen_line_starting_posB = {1} - local pos = 1 - for _, f in ipairs(line_cache.fragmentsB) do - local frag, frag_text = f.data, f.text - -- render fragment - local frag_width = App.width(frag_text) - if x + frag_width > State.right then - x = State.left - table.insert(line_cache.screen_line_starting_posB, pos) - end - x = x + frag_width - local frag_len = utf8.len(frag) - pos = pos + frag_len - end -end - -function Text.compute_fragmentsB(State, line_index, x) ---? print('compute_fragmentsB', line_index, 'between', x, State.right) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - if line_cache.fragmentsB then - return - end - line_cache.fragmentsB = {} - -- try to wrap at word boundaries - for frag in line.dataB: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') - while x + frag_width > State.right do ---? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x)) - if (x-State.left) < 0.8 * (State.right-State.left) then ---? print('splitting') - -- long word; chop it at some letter - -- We're not going to reimplement TeX here. - local bpos = Text.nearest_pos_less_than(frag, State.right - x) ---? print('bpos', bpos) - if bpos == 0 then break end -- avoid infinite loop when window is too narrow - local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos ---? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes') - local frag1 = string.sub(frag, 1, boffset-1) - local frag1_text = App.newText(love.graphics.getFont(), frag1) - local frag1_width = App.width(frag1_text) ---? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px') - assert(x + frag1_width <= State.right) - table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text}) - frag = string.sub(frag, boffset) - frag_text = App.newText(love.graphics.getFont(), frag) - frag_width = App.width(frag_text) - end - x = State.left -- new line - end - if #frag > 0 then ---? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px') - table.insert(line_cache.fragmentsB, {data=frag, text=frag_text}) - end - x = x + frag_width - end -end - function Text.text_input(State, t) if App.mouse_down(1) then return end if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end @@ -367,18 +178,10 @@ function Text.text_input(State, t) end function Text.insert_at_cursor(State, t) - if State.cursor1.pos then - 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 - else - assert(State.cursor1.posB) - local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB) - State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset) - Text.clear_screen_line_cache(State, State.cursor1.line) - State.cursor1.posB = State.cursor1.posB+1 - end + local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) + State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset) + Text.clear_screen_line_cache(State, State.cursor1.line) + State.cursor1.pos = State.cursor1.pos+1 end -- Don't handle any keys here that would trigger text_input above. @@ -413,7 +216,7 @@ function Text.keychord_press(State, chord) return end local before - if State.cursor1.pos and State.cursor1.pos > 1 then + if State.cursor1.pos > 1 then before = snapshot(State, State.cursor1.line) local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1) local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) @@ -425,22 +228,6 @@ function Text.keychord_press(State, chord) end State.cursor1.pos = State.cursor1.pos-1 end - elseif State.cursor1.posB then - if State.cursor1.posB > 1 then - before = snapshot(State, State.cursor1.line) - local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1) - local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB) - if byte_start then - if byte_end then - State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end) - else - State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1) - end - State.cursor1.posB = State.cursor1.posB-1 - end - else - -- refuse to delete past beginning of side B - end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) if State.lines[State.cursor1.line-1].mode == 'drawing' then @@ -476,12 +263,12 @@ function Text.keychord_press(State, chord) return end local before - if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) 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 and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then + 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 @@ -492,27 +279,10 @@ function Text.keychord_press(State, chord) end -- no change to State.cursor1.pos end - elseif State.cursor1.posB then - if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then - local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB) - local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1) - if byte_start then - if byte_end then - State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end) - else - State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1) - end - -- no change to State.cursor1.pos - end - else - -- refuse to delete past end of side B - end elseif State.cursor1.line < #State.lines then if State.lines[State.cursor1.line+1].mode == 'text' then -- join lines State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - -- delete side B on first line - State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB end table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) @@ -529,12 +299,12 @@ function Text.keychord_press(State, chord) 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.right(State) -- C- hotkeys reserved for drawings, so we'll use M- @@ -546,12 +316,12 @@ function Text.keychord_press(State, chord) 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.word_right(State) elseif chord == 'home' then @@ -562,12 +332,12 @@ function Text.keychord_press(State, chord) 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.end_of_line(State) elseif chord == 'up' then @@ -578,12 +348,12 @@ function Text.keychord_press(State, chord) 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.down(State) elseif chord == 'pageup' then @@ -594,30 +364,24 @@ function Text.keychord_press(State, chord) 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} 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} + State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos} end Text.pagedown(State) end end function Text.insert_return(State) - if State.cursor1.pos then - -- when inserting a newline, move any B side to the new line - local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB}) - table.insert(State.line_cache, State.cursor1.line+1, {}) - State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1) - State.lines[State.cursor1.line].dataB = nil - Text.clear_screen_line_cache(State, State.cursor1.line) - State.cursor1 = {line=State.cursor1.line+1, pos=1} - else - -- disable enter when cursor is on the B side - end + local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) + table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) + table.insert(State.line_cache, State.cursor1.line+1, {}) + State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1) + Text.clear_screen_line_cache(State, State.cursor1.line) + State.cursor1 = {line=State.cursor1.line+1, pos=1} end function Text.pageup(State) @@ -628,7 +392,7 @@ function Text.pageup(State) local y = App.screen.height - State.line_height while y >= State.top do --? print(y, top2.line, top2.screen_line, top2.screen_pos) - if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break end + if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end if State.lines[State.screen_top1.line].mode == 'text' then y = y - State.line_height elseif State.lines[State.screen_top1.line].mode == 'drawing' then @@ -637,7 +401,7 @@ function Text.pageup(State) top2 = Text.previous_screen_line(State, top2) end State.screen_top1 = Text.to1(State, top2) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB} + State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) --? print('pageup end') @@ -645,15 +409,21 @@ end function Text.pagedown(State) --? print('pagedown') + -- If a line/paragraph gets to a page boundary, I often want to scroll + -- before I get to the bottom. + -- However, only do this if it makes forward progress. local bot2 = Text.to2(State, State.screen_bottom1) + if bot2.screen_line > 1 then + bot2.screen_line = math.max(bot2.screen_line-10, 1) + end local new_top1 = Text.to1(State, bot2) if Text.lt1(State.screen_top1, new_top1) then State.screen_top1 = new_top1 else - State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB} + State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos} end --? print('setting top to', State.screen_top1.line, State.screen_top1.pos) - State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB} + State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) --? print('top now', State.screen_top1.line) Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks @@ -662,14 +432,6 @@ end function Text.up(State) assert(State.lines[State.cursor1.line].mode == 'text') - if State.cursor1.pos then - Text.upA(State) - else - Text.upB(State) - end -end - -function Text.upA(State) --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) if screen_line_starting_pos == 1 then @@ -709,62 +471,6 @@ function Text.upA(State) end end -function Text.upB(State) - local line_cache = State.line_cache[State.cursor1.line] - local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1) - assert(screen_line_indexB >= 1) - if screen_line_indexB == 1 then - -- move to A side of previous line - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = {line=new_cursor_line, posB=nil} - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - local prev_line_cache = State.line_cache[State.cursor1.line] - local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos] - local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset) - State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 - break - end - end - elseif screen_line_indexB == 2 then - -- all-B screen-line to potentially A+B screen-line - local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding - if State.cursor_x < xA then - State.cursor1.posB = nil - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos] - 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 - else - Text.populate_screen_line_starting_posB(State, State.cursor1.line) - local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1] - local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB) - local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB) - State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1 - end - else - assert(screen_line_indexB > 2) - -- all-B screen-line to all-B screen-line - Text.populate_screen_line_starting_posB(State, State.cursor1.line) - local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1] - local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB) - local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB) - State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 - end - if Text.lt1(State.cursor1, State.screen_top1) then - local top2 = Text.to2(State, State.screen_top1) - top2 = Text.previous_screen_line(State, top2) - State.screen_top1 = Text.to1(State, top2) - end -end - --- cursor on final screen line (A or B side) => goes to next screen line on A side --- cursor on A side => move down one screen line (A side) in current line --- cursor on B side => move down one screen line (B side) in current line function Text.down(State) assert(State.lines[State.cursor1.line].mode == 'text') --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) @@ -789,8 +495,8 @@ function Text.down(State) Text.snap_cursor_to_bottom_of_screen(State) --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos) end - elseif State.cursor1.pos then - -- move down one screen line (A side) in current line + else + -- move down one screen line in current line local scroll_down = Text.le1(State.screen_bottom1, State.cursor1) --? print('cursor is NOT at final screen line of its line') local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) @@ -806,85 +512,26 @@ function Text.down(State) Text.snap_cursor_to_bottom_of_screen(State) --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos) end - else - -- move down one screen line (B side) in current line - local scroll_down = false - if Text.le1(State.screen_bottom1, State.cursor1) then - scroll_down = true - end - local cursor_line = State.lines[State.cursor1.line] - local cursor_line_cache = State.line_cache[State.cursor1.line] - local cursor2 = Text.to2(State, State.cursor1) - assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB) - local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1) - Text.populate_screen_line_starting_posB(State, State.cursor1.line) - local new_screen_line_starting_posB = cursor_line_cache.screen_line_starting_posB[screen_line_indexB+1] - local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB) - local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB) - State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 - if scroll_down then - Text.snap_cursor_to_bottom_of_screen(State) - end end --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) end function Text.start_of_line(State) - if State.cursor1.pos then - State.cursor1.pos = 1 - else - State.cursor1.posB = 1 - end + State.cursor1.pos = 1 if Text.lt1(State.cursor1, State.screen_top1) then - State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copy + State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy end end function Text.end_of_line(State) - if State.cursor1.pos then - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 - else - State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1 - end + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 if Text.cursor_out_of_screen(State) then Text.snap_cursor_to_bottom_of_screen(State) end end function Text.word_left(State) - -- we can cross the fold, so check side A/B one level down - Text.skip_whitespace_left(State) - Text.left(State) - Text.skip_non_whitespace_left(State) -end - -function Text.word_right(State) - -- we can cross the fold, so check side A/B one level down - Text.skip_whitespace_right(State) - Text.right(State) - Text.skip_non_whitespace_right(State) - if Text.cursor_out_of_screen(State) then - Text.snap_cursor_to_bottom_of_screen(State) - end -end - -function Text.skip_whitespace_left(State) - if State.cursor1.pos then - Text.skip_whitespace_leftA(State) - else - Text.skip_whitespace_leftB(State) - end -end - -function Text.skip_non_whitespace_left(State) - if State.cursor1.pos then - Text.skip_non_whitespace_leftA(State) - else - Text.skip_non_whitespace_leftB(State) - end -end - -function Text.skip_whitespace_leftA(State) + -- skip some whitespace while true do if State.cursor1.pos == 1 then break @@ -894,22 +541,9 @@ function Text.skip_whitespace_leftA(State) end Text.left(State) end -end - -function Text.skip_whitespace_leftB(State) + -- skip some non-whitespace while true do - if State.cursor1.posB == 1 then - break - end - if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') then - break - end Text.left(State) - end -end - -function Text.skip_non_whitespace_leftA(State) - while true do if State.cursor1.pos == 1 then break end @@ -917,40 +551,11 @@ function Text.skip_non_whitespace_leftA(State) if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then break end - Text.left(State) end end -function Text.skip_non_whitespace_leftB(State) - while true do - if State.cursor1.posB == 1 then - break - end - assert(State.cursor1.posB > 1) - if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then - break - end - Text.left(State) - end -end - -function Text.skip_whitespace_right(State) - if State.cursor1.pos then - Text.skip_whitespace_rightA(State) - else - Text.skip_whitespace_rightB(State) - end -end - -function Text.skip_non_whitespace_right(State) - if State.cursor1.pos then - Text.skip_non_whitespace_rightA(State) - else - Text.skip_non_whitespace_rightB(State) - end -end - -function Text.skip_whitespace_rightA(State) +function Text.word_right(State) + -- skip some whitespace while true do if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then break @@ -960,41 +565,17 @@ function Text.skip_whitespace_rightA(State) end Text.right_without_scroll(State) end -end - -function Text.skip_whitespace_rightB(State) while true do - if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then - break - end - if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') then - break - end Text.right_without_scroll(State) - end -end - -function Text.skip_non_whitespace_rightA(State) - while true do if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then break end if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then break end - Text.right_without_scroll(State) end -end - -function Text.skip_non_whitespace_rightB(State) - while true do - if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then - break - end - if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then - break - end - Text.right_without_scroll(State) + if Text.cursor_out_of_screen(State) then + Text.snap_cursor_to_bottom_of_screen(State) end end @@ -1008,14 +589,7 @@ function Text.match(s, pos, pat) end function Text.left(State) - if State.cursor1.pos then - Text.leftA(State) - else - Text.leftB(State) - end -end - -function Text.leftA(State) + assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos > 1 then State.cursor1.pos = State.cursor1.pos-1 else @@ -1038,21 +612,6 @@ function Text.leftA(State) end end -function Text.leftB(State) - if State.cursor1.posB > 1 then - State.cursor1.posB = State.cursor1.posB-1 - else - -- overflow back into A side - State.cursor1.posB = nil - 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) - top2 = Text.previous_screen_line(State, top2) - State.screen_top1 = Text.to1(State, top2) - end -end - function Text.right(State) Text.right_without_scroll(State) if Text.cursor_out_of_screen(State) then @@ -1062,14 +621,6 @@ end function Text.right_without_scroll(State) assert(State.lines[State.cursor1.line].mode == 'text') - if State.cursor1.pos then - Text.right_without_scrollA(State) - else - Text.right_without_scrollB(State) - end -end - -function Text.right_without_scrollA(State) if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then State.cursor1.pos = State.cursor1.pos+1 else @@ -1084,22 +635,6 @@ function Text.right_without_scrollA(State) end end -function Text.right_without_scrollB(State) - if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then - State.cursor1.posB = State.cursor1.posB+1 - else - -- overflow back into A side - local new_cursor_line = State.cursor1.line - while new_cursor_line <= #State.lines-1 do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1 = {line=new_cursor_line, pos=1} - break - end - end - end -end - function Text.pos_at_start_of_screen_line(State, loc1) Text.populate_screen_line_starting_pos(State, loc1.line) local line_cache = State.line_cache[loc1.line] @@ -1112,39 +647,11 @@ function Text.pos_at_start_of_screen_line(State, loc1) assert(false) end -function Text.pos_at_start_of_screen_lineB(State, loc1) - Text.populate_screen_line_starting_pos(State, loc1.line) - local line_cache = State.line_cache[loc1.line] - local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding - Text.populate_screen_line_starting_posB(State, loc1.line, x) - for i=#line_cache.screen_line_starting_posB,1,-1 do - local sposB = line_cache.screen_line_starting_posB[i] - if sposB <= loc1.posB then - return sposB,i - end - end - assert(false) -end - function Text.cursor_at_final_screen_line(State) Text.populate_screen_line_starting_pos(State, State.cursor1.line) - local line = State.lines[State.cursor1.line] local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos --? print(screen_lines[#screen_lines], State.cursor1.pos) - if (not State.expanded and not line.expanded) or - line.dataB == nil then - return screen_lines[#screen_lines] <= State.cursor1.pos - end - if State.cursor1.pos then - -- ignore B side - return screen_lines[#screen_lines] <= State.cursor1.pos - end - assert(State.cursor1.posB) - local line_cache = State.line_cache[State.cursor1.line] - local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding - Text.populate_screen_line_starting_posB(State, State.cursor1.line, x) - local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB - return screen_lines[#screen_lines] <= State.cursor1.posB + return screen_lines[#screen_lines] <= State.cursor1.pos end function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) @@ -1172,22 +679,17 @@ end -- should never modify State.cursor1 function Text.snap_cursor_to_bottom_of_screen(State) ---? print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB) +--? print('to2:', State.cursor1.line, State.cursor1.pos) local top2 = Text.to2(State, State.cursor1) ---? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB) +--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos) -- slide to start of screen line - if top2.screen_pos then - top2.screen_pos = 1 - else - assert(top2.screen_posB) - top2.screen_posB = 1 - end ---? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB) + top2.screen_pos = 1 -- start of screen line +--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down') local y = App.screen.height - State.line_height -- duplicate some logic from love.draw while true do ---? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB) +--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos) if top2.line == 1 and top2.screen_line == 1 then break end if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then local h = State.line_height @@ -1212,7 +714,7 @@ function Text.snap_cursor_to_bottom_of_screen(State) --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) State.screen_top1 = Text.to1(State, top2) --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos) ---? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB) +--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks end @@ -1221,116 +723,34 @@ function Text.in_line(State, line_index, x,y) local line_cache = State.line_cache[line_index] if line_cache.starty == nil then return false end -- outside current page if y < line_cache.starty then return false end - local num_screen_lines = 0 - if line_cache.startpos then - Text.populate_screen_line_starting_pos(State, line_index) - num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1 - end ---? print('#screenlines after A', num_screen_lines) - if line.dataB and (State.expanded or line.expanded) then - local x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding - Text.populate_screen_line_starting_posB(State, line_index, x) ---? print('B:', x, #line_cache.screen_line_starting_posB) - if line_cache.startposB then - num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) -- no +1; first screen line of B side overlaps with A side - else - num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1) -- no +1; first screen line of B side overlaps with A side - end - end ---? print('#screenlines after B', num_screen_lines) - return y < line_cache.starty + State.line_height*num_screen_lines + Text.populate_screen_line_starting_pos(State, line_index) + return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1) end -- convert mx,my in pixels to schema-1 coordinates --- returns: pos, posB --- scenarios: --- line without B side --- line with B side collapsed --- line with B side expanded --- line starting rendering in A side (startpos ~= nil) --- line starting rendering in B side (startposB ~= nil) --- my on final screen line of A side --- mx to right of A side with no B side --- mx to right of A side but left of B side --- mx to right of B side --- preconditions: --- startpos xor startposB --- expanded -> dataB function Text.to_pos_on_line(State, line_index, mx, my) local line = State.lines[line_index] local line_cache = State.line_cache[line_index] assert(my >= line_cache.starty) -- duplicate some logic from Text.draw local y = line_cache.starty ---? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB) - if line_cache.startpos then - 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.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 - -- line position cursor on final character of screen line. - -- (The final screen line positions past end of screen line as always.) - if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then ---? 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.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) - local screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left) - if line.dataB == nil then - -- no B side - return screen_line_starting_pos + screen_line_posA - 1 - end - if not State.expanded and not line.expanded then - -- B side is not expanded - return screen_line_starting_pos + screen_line_posA - 1 - end - local lenA = utf8.len(s) - if screen_line_posA < lenA then - -- mx is within A side - return screen_line_starting_pos + screen_line_posA - 1 - end - local max_xA = State.left+Text.x(s, lenA+1) - if mx < max_xA + AB_padding then - -- mx is in the space between A and B side - return screen_line_starting_pos + screen_line_posA - 1 - end - mx = mx - max_xA - AB_padding - local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0) - return nil, screen_line_posB - end - y = nexty - end - end - -- look in screen lines composed entirely of the B side - assert(State.expanded or line.expanded) - local start_screen_line_indexB - if line_cache.startposB then - start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) - else - start_screen_line_indexB = 2 -- skip the first line of side B, which we checked above - end - for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do - local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB] - local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB) ---? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB)) + 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.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 -- line position cursor on final character of screen line. -- (The final screen line positions past end of screen line as always.) ---? print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB)) - if screen_line_indexB < #line_cache.screen_line_starting_posB and mx > State.left + Text.screen_line_widthB(State, line_index, screen_line_indexB) then + if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then --? print('past end of non-final line; return') - return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1 + return line_cache.screen_line_starting_pos[screen_line_index+1]-1 end - local s = string.sub(line.dataB, screen_line_starting_byte_offsetB) ---? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1) - return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1 + 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 y = nexty end @@ -1354,30 +774,6 @@ function Text.screen_line_width(State, line_index, i) return App.width(screen_line_text) end -function Text.screen_line_widthB(State, line_index, i) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - local start_posB = line_cache.screen_line_starting_posB[i] - local start_offsetB = Text.offset(line.dataB, start_posB) - local screen_line - if i < #line_cache.screen_line_starting_posB then ---? print('non-final', i) - local past_end_posB = line_cache.screen_line_starting_posB[i+1] - local past_end_offsetB = Text.offset(line.dataB, past_end_posB) ---? print('between', start_offsetB, past_end_offsetB) - screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1) - else ---? print('final', i) ---? print('after', start_offsetB) - screen_line = string.sub(line.dataB, start_offsetB) - end - local screen_line_text = App.newText(love.graphics.getFont(), screen_line) ---? local result = App.width(screen_line_text) ---? print('=>', result) ---? return result - return App.width(screen_line_text) -end - function Text.screen_line_index(screen_line_starting_pos, pos) for i = #screen_line_starting_pos,1,-1 do if screen_line_starting_pos[i] <= pos then @@ -1386,18 +782,6 @@ function Text.screen_line_index(screen_line_starting_pos, pos) end end -function Text.screen_line_indexB(screen_line_starting_posB, posB) - if posB == nil then - return 0 - end - assert(screen_line_starting_posB) - for i = #screen_line_starting_posB,1,-1 do - if screen_line_starting_posB[i] <= posB then - return i - end - end -end - -- convert x pixel coordinate to pos -- oblivious to wrapping -- result: 1 to len+1 @@ -1490,14 +874,6 @@ function Text.to2(State, loc1) if State.lines[loc1.line].mode == 'drawing' then return {line=loc1.line, screen_line=1, screen_pos=1} end - if loc1.pos then - return Text.to2A(State, loc1) - else - return Text.to2B(State, loc1) - end -end - -function Text.to2A(State, loc1) local result = {line=loc1.line} local line_cache = State.line_cache[loc1.line] Text.populate_screen_line_starting_pos(State, loc1.line) @@ -1513,33 +889,7 @@ function Text.to2A(State, loc1) return result end -function Text.to2B(State, loc1) - local result = {line=loc1.line} - local line_cache = State.line_cache[loc1.line] - Text.populate_screen_line_starting_pos(State, loc1.line) - local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding - Text.populate_screen_line_starting_posB(State, loc1.line, x) - for i=#line_cache.screen_line_starting_posB,1,-1 do - local sposB = line_cache.screen_line_starting_posB[i] - if sposB <= loc1.posB then - result.screen_lineB = i - result.screen_posB = loc1.posB - sposB + 1 - break - end - end - assert(result.screen_posB) - return result -end - function Text.to1(State, loc2) - if loc2.screen_pos then - return Text.to1A(State, loc2) - else - return Text.to1B(State, loc2) - end -end - -function Text.to1A(State, loc2) local result = {line=loc2.line, pos=loc2.screen_pos} if loc2.screen_line > 1 then result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1 @@ -1547,12 +897,8 @@ function Text.to1A(State, loc2) return result end -function Text.to1B(State, loc2) - local result = {line=loc2.line, posB=loc2.screen_posB} - if loc2.screen_lineB > 1 then - result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1 - end - return result +function Text.eq1(a, b) + return a.line == b.line and a.pos == b.pos end function Text.lt1(a, b) @@ -1562,22 +908,17 @@ function Text.lt1(a, b) if a.line > b.line then return false end - -- A side < B side - if a.pos and not b.pos then - return true - end - if not a.pos and b.pos then - return false - end - if a.pos then - return a.pos < b.pos - else - return a.posB < b.posB - end + return a.pos < b.pos end function Text.le1(a, b) - return eq(a, b) or Text.lt1(a, b) + if a.line < b.line then + return true + end + if a.line > b.line then + return false + end + return a.pos <= b.pos end function Text.offset(s, pos1) @@ -1591,49 +932,16 @@ function Text.offset(s, pos1) end function Text.previous_screen_line(State, loc2) - if loc2.screen_pos then - return Text.previous_screen_lineA(State, loc2) - else - return Text.previous_screen_lineB(State, loc2) - end -end - -function Text.previous_screen_lineA(State, loc2) if loc2.screen_line > 1 then return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1} elseif loc2.line == 1 then return loc2 + elseif State.lines[loc2.line-1].mode == 'drawing' then + return {line=loc2.line-1, screen_line=1, screen_pos=1} else + local l = State.lines[loc2.line-1] Text.populate_screen_line_starting_pos(State, loc2.line-1) - if State.lines[loc2.line-1].dataB == nil or - (not State.expanded and not State.lines[loc2.line-1].expanded) then ---? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB) - return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1} - end - -- try to switch to B - local prev_line_cache = State.line_cache[loc2.line-1] - local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding - Text.populate_screen_line_starting_posB(State, loc2.line-1, x) - local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB ---? print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x) - if #screen_line_starting_posB > 1 then ---? print('c2') - return {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1} - else ---? print('c3') - -- if there's only one screen line, assume it overlaps with A, so remain in A - return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1} - end - end -end - -function Text.previous_screen_lineB(State, loc2) - if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A side - return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1} - else - -- switch to A side - -- TODO: handle case where fold lands precisely at end of a new screen-line - return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1} + return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1} end end @@ -1666,8 +974,10 @@ function Text.tweak_screen_top_and_cursor(State) --? print('too low') if Text.cursor_out_of_screen(State) then --? print('tweak') - local pos,posB = Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5) - State.cursor1 = {line=State.screen_bottom1.line, pos=pos, posB=posB} + State.cursor1 = { + line=State.screen_bottom1.line, + pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5), + } end end end @@ -1704,9 +1014,7 @@ end function Text.clear_screen_line_cache(State, line_index) State.line_cache[line_index].fragments = nil - State.line_cache[line_index].fragmentsB = nil State.line_cache[line_index].screen_line_starting_pos = nil - State.line_cache[line_index].screen_line_starting_posB = nil end function trim(s) diff --git a/text.lua b/text.lua index 13ef07d..1267699 100644 --- a/text.lua +++ b/text.lua @@ -156,7 +156,6 @@ function Text.text_input(State, t) if State.cursor_y > App.screen.height - State.line_height then Text.populate_screen_line_starting_pos(State, State.cursor1.line) Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right) ---? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) end record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)}) end