Merge lines.love

This commit is contained in:
Kartik K. Agaram 2023-06-04 15:11:24 -07:00
commit bf2c2555d6
12 changed files with 402 additions and 58 deletions

View File

@ -361,6 +361,7 @@ function App.run_tests()
for _,name in ipairs(sorted_names) do for _,name in ipairs(sorted_names) do
App.initialize_for_test() App.initialize_for_test()
--? print('=== '..name) --? print('=== '..name)
--? _G[name]()
xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end) xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end)
end end
-- clean up all test methods -- clean up all test methods

View File

@ -43,8 +43,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c
old_cursor1 = nil, old_cursor1 = nil,
old_selection1 = nil, old_selection1 = nil,
mousepress_shift = nil, mousepress_shift = nil,
-- when selecting text, avoid recomputing some state on every single frame
recent_mouse = {},
-- cursor coordinates in pixels -- cursor coordinates in pixels
cursor_x = 0, cursor_x = 0,
@ -103,20 +101,22 @@ function edit.draw(State)
State.cursor_x = nil State.cursor_x = nil
State.cursor_y = nil State.cursor_y = nil
local y = State.top local y = State.top
local screen_bottom1 = {line=nil, pos=nil}
--? print('== draw') --? print('== draw')
for line_index = State.screen_top1.line,#State.lines do for line_index = State.screen_top1.line,#State.lines do
local line = State.lines[line_index] local line = State.lines[line_index]
--? print('draw:', y, line_index, line) --? print('draw:', y, line_index, line)
if y + State.line_height > App.screen.height then break end if y + State.line_height > App.screen.height then break end
State.screen_bottom1 = {line=line_index, pos=nil} screen_bottom1.line = line_index
--? print('text.draw', y, line_index) --? print('text.draw', y, line_index)
local startpos = 1 local startpos = 1
if line_index == State.screen_top1.line then if line_index == State.screen_top1.line then
startpos = State.screen_top1.pos startpos = State.screen_top1.pos
end end
y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos) y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
--? print('=> y', y) --? print('=> y', y)
end end
State.screen_bottom1 = screen_bottom1
if State.search_term then if State.search_term then
Text.draw_search_bar(State) Text.draw_search_bar(State)
end end
@ -146,7 +146,18 @@ end
function edit.mouse_press(State, x,y, mouse_button) function edit.mouse_press(State, x,y, mouse_button)
if State.search_term then return end if State.search_term then return end
--? print('press', State.cursor1.line) --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
if y < State.top then
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=State.screen_top1.line,
pos=State.screen_top1.pos,
}
return
end
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
-- delicate dance between cursor, selection and old cursor/selection -- delicate dance between cursor, selection and old cursor/selection
@ -157,7 +168,8 @@ function edit.mouse_press(State, x,y, mouse_button)
-- sets cursor -- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release -- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift -- press and hold, then press shift: ignore shift
-- i.e. mouse_released should never look at shift state -- i.e. mouse_release should never look at shift state
--? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
State.old_cursor1 = State.cursor1 State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1 State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down() State.mousepress_shift = App.shift_down()
@ -165,23 +177,31 @@ function edit.mouse_press(State, x,y, mouse_button)
line=line_index, line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y), pos=Text.to_pos_on_line(State, line_index, x, y),
} }
--? print('selection', State.selection1.line, State.selection1.pos) return
break
end end
end end
-- still here? click is below all screen lines
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=State.screen_bottom1.line,
pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
}
end end
function edit.mouse_release(State, x,y, mouse_button) function edit.mouse_release(State, x,y, mouse_button)
if State.search_term then return end if State.search_term then return end
--? print('release', State.cursor1.line) --? print_and_log(('edit.mouse_release(%d,%d): cursor at %d,%d'):format(x,y, State.cursor1.line, State.cursor1.pos))
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
--? print('reset selection') --? print_and_log(('edit.mouse_release: in line %d'):format(line_index))
State.cursor1 = { State.cursor1 = {
line=line_index, line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y), pos=Text.to_pos_on_line(State, line_index, x, y),
} }
--? print('cursor', State.cursor1.line, State.cursor1.pos) --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
if State.mousepress_shift then if State.mousepress_shift then
if State.old_selection1.line == nil then if State.old_selection1.line == nil then
State.selection1 = State.old_cursor1 State.selection1 = State.old_cursor1
@ -196,7 +216,7 @@ function edit.mouse_release(State, x,y, mouse_button)
break break
end end
end end
--? print('selection:', State.selection1.line, State.selection1.pos) --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
end end
function edit.mouse_wheel_move(State, dx,dy) function edit.mouse_wheel_move(State, dx,dy)

View File

@ -48,6 +48,11 @@ function run.initialize(arg)
end end
end end
function print_and_log(s)
print(s)
log(3, s)
end
function run.load_settings() function run.load_settings()
love.graphics.setFont(love.graphics.newFont(Settings.font_height)) love.graphics.setFont(love.graphics.newFont(Settings.font_height))
-- determine default dimensions and flags -- determine default dimensions and flags

View File

@ -29,7 +29,6 @@ function Text.clip_selection(State, line_index, apos, bpos)
-- compare bounds more carefully (start inclusive, end exclusive) -- compare bounds more carefully (start inclusive, end exclusive)
local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos}) local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})
local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp}) local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})
--? print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)
if a_ge and b_lt then if a_ge and b_lt then
-- fully contained -- fully contained
return apos,bpos return apos,bpos
@ -60,7 +59,6 @@ function Text.draw_highlight(State, line, x,y, pos, lo,hi)
local before = line.data:sub(pos_offset, lo_offset-1) local before = line.data:sub(pos_offset, lo_offset-1)
lo_px = App.width(before) lo_px = App.width(before)
end end
--? print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px)
local s = line.data:sub(lo_offset, hi_offset-1) local s = line.data:sub(lo_offset, hi_offset-1)
App.color(Highlight_color) App.color(Highlight_color)
love.graphics.rectangle('fill', x+lo_px,y, App.width(s),State.line_height) love.graphics.rectangle('fill', x+lo_px,y, App.width(s),State.line_height)
@ -69,27 +67,17 @@ function Text.draw_highlight(State, line, x,y, pos, lo,hi)
end end
end end
-- inefficient for some reason, so don't do it on every frame
function Text.mouse_pos(State) function Text.mouse_pos(State)
local time = love.timer.getTime() local x,y = App.mouse_x(), App.mouse_y()
if State.recent_mouse.time and State.recent_mouse.time > time-0.1 then if y < State.line_cache[State.screen_top1.line].starty then
return State.recent_mouse.line, State.recent_mouse.pos return State.screen_top1.line, State.screen_top1.pos
end end
State.recent_mouse.time = time
local line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())
if line then
State.recent_mouse.line = line
State.recent_mouse.pos = pos
end
return State.recent_mouse.line, State.recent_mouse.pos
end
function Text.to_pos(State, x,y)
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
return line_index, Text.to_pos_on_line(State, line_index, x,y) return line_index, Text.to_pos_on_line(State, line_index, x,y)
end end
end end
return State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
end end
function Text.cut_selection(State) function Text.cut_selection(State)

View File

@ -113,6 +113,11 @@ function source.initialize_edit_side()
end end
end end
function print_and_log(s)
print(s)
log(3, s)
end
function source.load_settings() function source.load_settings()
local settings = Settings.source local settings = Settings.source
love.graphics.setFont(love.graphics.newFont(settings.font_height)) love.graphics.setFont(love.graphics.newFont(settings.font_height))

View File

@ -76,8 +76,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c
old_cursor1 = nil, old_cursor1 = nil,
old_selection1 = nil, old_selection1 = nil,
mousepress_shift = nil, mousepress_shift = nil,
-- when selecting text, avoid recomputing some state on every single frame
recent_mouse = {},
-- cursor coordinates in pixels -- cursor coordinates in pixels
cursor_x = 0, cursor_x = 0,
@ -155,12 +153,13 @@ function edit.draw(State, hide_cursor)
State.cursor_x = nil State.cursor_x = nil
State.cursor_y = nil State.cursor_y = nil
local y = State.top local y = State.top
local screen_bottom1 = {line=nil, pos=nil}
--? print('== draw') --? print('== draw')
for line_index = State.screen_top1.line,#State.lines do for line_index = State.screen_top1.line,#State.lines do
local line = State.lines[line_index] local line = State.lines[line_index]
--? print('draw:', y, line_index, line) --? print('draw:', y, line_index, line)
if y + State.line_height > App.screen.height then break end if y + State.line_height > App.screen.height then break end
State.screen_bottom1 = {line=line_index, pos=nil} screen_bottom1.line = line_index
if line.mode == 'text' then if line.mode == 'text' then
--? print('text.draw', y, line_index) --? print('text.draw', y, line_index)
local startpos = 1 local startpos = 1
@ -183,7 +182,7 @@ function edit.draw(State, hide_cursor)
end, end,
}) })
end end
y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos, hide_cursor) y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos, hide_cursor)
--? print('=> y', y) --? print('=> y', y)
elseif line.mode == 'drawing' then elseif line.mode == 'drawing' then
y = y+Drawing_padding_top y = y+Drawing_padding_top
@ -194,6 +193,7 @@ function edit.draw(State, hide_cursor)
assert(false) assert(false)
end end
end end
State.screen_bottom1 = screen_bottom1
if State.search_term then if State.search_term then
Text.draw_search_bar(State) Text.draw_search_bar(State)
end end
@ -224,12 +224,23 @@ end
function edit.mouse_press(State, x,y, mouse_button) function edit.mouse_press(State, x,y, mouse_button)
if State.search_term then return end if State.search_term then return end
--? print('press', State.cursor1.line) --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then
-- press on a button and it returned 'true' to short-circuit -- press on a button and it returned 'true' to short-circuit
return return
end end
if y < State.top then
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=State.screen_top1.line,
pos=State.screen_top1.pos,
}
return
end
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
@ -242,6 +253,7 @@ function edit.mouse_press(State, x,y, mouse_button)
-- press and hold to start a selection: sets selection on press, cursor on release -- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift -- press and hold, then press shift: ignore shift
-- i.e. mouse_release should never look at shift state -- i.e. mouse_release should never look at shift state
--? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
State.old_cursor1 = State.cursor1 State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1 State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down() State.mousepress_shift = App.shift_down()
@ -249,8 +261,7 @@ function edit.mouse_press(State, x,y, mouse_button)
line=line_index, line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y), pos=Text.to_pos_on_line(State, line_index, x, y),
} }
--? print('selection', State.selection1.line, State.selection1.pos) return
break
end end
elseif line.mode == 'drawing' then elseif line.mode == 'drawing' then
local line_cache = State.line_cache[line_index] local line_cache = State.line_cache[line_index]
@ -259,15 +270,24 @@ function edit.mouse_press(State, x,y, mouse_button)
State.lines.current_drawing = line State.lines.current_drawing = line
Drawing.before = snapshot(State, line_index) Drawing.before = snapshot(State, line_index)
Drawing.mouse_press(State, line_index, x,y, mouse_button) Drawing.mouse_press(State, line_index, x,y, mouse_button)
break return
end end
end end
end end
-- still here? click is below all screen lines
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=State.screen_bottom1.line,
pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
}
end end
function edit.mouse_release(State, x,y, mouse_button) function edit.mouse_release(State, x,y, mouse_button)
if State.search_term then return end if State.search_term then return end
--? print('release', State.cursor1.line) --? print_and_log(('edit.mouse_release: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
if State.lines.current_drawing then if State.lines.current_drawing then
Drawing.mouse_release(State, x,y, mouse_button) Drawing.mouse_release(State, x,y, mouse_button)
schedule_save(State) schedule_save(State)
@ -276,15 +296,16 @@ function edit.mouse_release(State, x,y, mouse_button)
Drawing.before = nil Drawing.before = nil
end end
else else
--? print_and_log('edit.mouse_release: no current drawing')
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
--? print('reset selection') --? print_and_log(('edit.mouse_release: in line %d'):format(line_index))
State.cursor1 = { State.cursor1 = {
line=line_index, line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y), pos=Text.to_pos_on_line(State, line_index, x, y),
} }
--? print('cursor', State.cursor1.line, State.cursor1.pos) --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
if State.mousepress_shift then if State.mousepress_shift then
if State.old_selection1.line == nil then if State.old_selection1.line == nil then
State.selection1 = State.old_cursor1 State.selection1 = State.old_cursor1
@ -300,7 +321,7 @@ function edit.mouse_release(State, x,y, mouse_button)
end end
end end
end end
--? print('selection:', State.selection1.line, State.selection1.pos) --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
end end
end end

View File

@ -1,6 +1,4 @@
-- helpers for selecting portions of text -- helpers for selecting portions of text
-- To keep things simple, we'll ignore the B side when selections start on the
-- A side, and stick to within a single B side selections start in.
-- Return any intersection of the region from State.selection1 to State.cursor1 (or -- Return any intersection of the region from State.selection1 to State.cursor1 (or
-- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and -- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and
@ -31,7 +29,6 @@ function Text.clip_selection(State, line_index, apos, bpos)
-- compare bounds more carefully (start inclusive, end exclusive) -- compare bounds more carefully (start inclusive, end exclusive)
local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos}) local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})
local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp}) local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})
--? print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)
if a_ge and b_lt then if a_ge and b_lt then
-- fully contained -- fully contained
return apos,bpos return apos,bpos
@ -62,7 +59,6 @@ function Text.draw_highlight(State, line, x,y, pos, lo,hi)
local before = line.data:sub(pos_offset, lo_offset-1) local before = line.data:sub(pos_offset, lo_offset-1)
lo_px = App.width(before) lo_px = App.width(before)
end end
--? print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px)
local s = line.data:sub(lo_offset, hi_offset-1) local s = line.data:sub(lo_offset, hi_offset-1)
App.color(Highlight_color) App.color(Highlight_color)
love.graphics.rectangle('fill', x+lo_px,y, App.width(s),State.line_height) love.graphics.rectangle('fill', x+lo_px,y, App.width(s),State.line_height)
@ -71,22 +67,11 @@ function Text.draw_highlight(State, line, x,y, pos, lo,hi)
end end
end end
-- inefficient for some reason, so don't do it on every frame
function Text.mouse_pos(State) function Text.mouse_pos(State)
local time = love.timer.getTime() local x,y = App.mouse_x(), App.mouse_y()
if State.recent_mouse.time and State.recent_mouse.time > time-0.1 then if y < State.line_cache[State.screen_top1.line].starty then
return State.recent_mouse.line, State.recent_mouse.pos return State.screen_top1.line, State.screen_top1.pos
end end
State.recent_mouse.time = time
local line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())
if line then
State.recent_mouse.line = line
State.recent_mouse.pos = pos
end
return State.recent_mouse.line, State.recent_mouse.pos
end
function Text.to_pos(State, x,y)
for line_index,line in ipairs(State.lines) do for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then if Text.in_line(State, line_index, x,y) then
@ -94,6 +79,7 @@ function Text.to_pos(State, x,y)
end end
end end
end end
return State.screen_bottom1.line, Text.pos_at_end_of_screen_line(State, State.screen_bottom1)
end end
function Text.cut_selection(State) function Text.cut_selection(State)

View File

@ -650,6 +650,7 @@ function Text.right_without_scroll(State)
end end
end end
-- result: pos, index of screen line
function Text.pos_at_start_of_screen_line(State, loc1) function Text.pos_at_start_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line) Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line] local line_cache = State.line_cache[loc1.line]
@ -662,6 +663,20 @@ function Text.pos_at_start_of_screen_line(State, loc1)
assert(false) assert(false)
end end
function Text.pos_at_end_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return most_recent_final_pos
end
most_recent_final_pos = spos-1
end
assert(false)
end
function Text.cursor_at_final_screen_line(State) function Text.cursor_at_final_screen_line(State)
Text.populate_screen_line_starting_pos(State, State.cursor1.line) Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos

View File

@ -1,4 +1,6 @@
-- major tests for text editing flows -- major tests for text editing flows
-- Arguably this should be called source_edit_tests.lua,
-- but that would mess up the git blame at this point.
function test_initial_state() function test_initial_state()
App.screen.init{width=120, height=60} App.screen.init{width=120, height=60}
@ -828,6 +830,67 @@ function test_select_text_using_mouse()
check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
end end
function test_select_text_using_mouse_starting_above_text()
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', 'def', 'xyz'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
Editor_state.selection1 = {}
edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 1, 'selection:pos')
end
function test_select_text_using_mouse_starting_above_text_wrapping_line()
-- first screen line starts in the middle of a line
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=2, pos=5}
Editor_state.screen_top1 = {line=2, pos=3}
Editor_state.screen_bottom1 = {}
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
-- selection is at screen top
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 2, 'selection:line')
check_eq(Editor_state.selection1.pos, 3, 'selection:pos')
end
function test_select_text_using_mouse_starting_below_text()
-- I'd like to test what happens when a mouse click is below some page of
-- text, potentially even in the middle of a line.
-- However, it's brittle to set up a text line boundary just right.
-- So I'm going to just check things below the bottom of the final line of
-- text when it's in the middle of the screen.
-- final screen line ends in the middle of screen
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abcde'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'ab', 'baseline:screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'cde', 'baseline:screen:2')
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, 5,App.screen.height-5, 1)
-- selection is past bottom-most text in screen
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 6, 'selection:pos')
end
function test_select_text_using_mouse_and_shift() function test_select_text_using_mouse_and_shift()
App.screen.init{width=50, height=60} App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state() Editor_state = edit.initialize_test_state()
@ -882,6 +945,28 @@ function test_select_text_repeatedly_using_mouse_and_shift()
check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
end end
function test_select_all_text()
-- display a single line of text
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc def'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
-- select all
App.fake_key_press('lctrl')
edit.run_after_keychord(Editor_state, 'C-a')
App.fake_key_release('lctrl')
edit.key_release(Editor_state, 'lctrl')
-- selection
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 1, 'selection:pos')
check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
check_eq(Editor_state.cursor1.pos, 8, 'cursor:pos')
end
function test_cut_without_selection() function test_cut_without_selection()
-- display a few lines -- display a few lines
App.screen.init{width=Editor_state.left+30, height=60} App.screen.init{width=Editor_state.left+30, height=60}

View File

@ -549,6 +549,7 @@ function Text.right_without_scroll(State)
end end
end end
-- result: pos, index of screen line
function Text.pos_at_start_of_screen_line(State, loc1) function Text.pos_at_start_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line) Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line] local line_cache = State.line_cache[loc1.line]
@ -561,6 +562,20 @@ function Text.pos_at_start_of_screen_line(State, loc1)
assert(false) assert(false)
end end
function Text.pos_at_end_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return most_recent_final_pos
end
most_recent_final_pos = spos-1
end
assert(false)
end
function Text.cursor_at_final_screen_line(State) function Text.cursor_at_final_screen_line(State)
Text.populate_screen_line_starting_pos(State, State.cursor1.line) Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos

118
text_tests Normal file
View File

@ -0,0 +1,118 @@
== Summary of tests for the text editor
This doesn't include all tests and might not change between forks, but it's
intended to be the "timeless core" of a text editor widget that shouldn't
change across forks.
# basic
initial state
draw text
draw wrapping text
draw word wrapping text
draw text wrapping within word
draw wrapping text containing non ascii
# mouse
click moves cursor
click to left of line
click takes margins into account
click on empty line
click on wrapping line
click on wrapping line takes margins into account
click on wrapping line
click on wrapping line rendered from partway at top of screen
click past end of wrapping line
click past end of wrapping line containing non ascii
click past end of word wrapping line
# cursor movement
move left
move left to previous line
move right
move right to next line
move to start of word
move to start of previous word
move to start of word on previous line
move past end of word
move past end of word on next line
skip to previous word
skip past tab to previous word
skip multiple spaces to previous word
skip to next word
skip past tab to next word
skip multiple spaces to next word
# mutating text
insert first character
edit wrapping text
insert newline
insert newline at start of line
insert from clipboard
backspace from start of final line
backspace past line boundary
backspace over selection
backspace over selection reverse
backspace over multiple lines
backspace to end of line
backspace to start of line
# scroll
pagedown
pagedown often shows start of wrapping line
pagedown can start from middle of long wrapping line
pagedown never moves up
down arrow moves cursor
down arrow scrolls down by one line
down arrow scrolls down by one screen line
down arrow scrolls down by one screen line after splitting within word
pagedown followed by down arrow does not scroll screen up
up arrow moves cursor
up arrow scrolls up by one line
up arrow scrolls up by one screen line
up arrow scrolls up to final screen line
up arrow scrolls up to empty line
pageup
pageup scrolls up by screen line
pageup scrolls up from middle screen line
enter on bottom line scrolls down
enter on final line avoids scrolling down when not at bottom
inserting text on final line avoids scrolling down when not at bottom
typing on bottom line scrolls down
left arrow scrolls up in wrapped line
right arrow scrolls down in wrapped line
home scrolls up in wrapped line
end scrolls down in wrapped line
position cursor on recently edited wrapping line
backspace can scroll up
backspace can scroll up screen line
# selection
select text using shift and cursor movement operations
select text using mouse
clicking to left of a line = start of line
clicking to right of a line = end of line
clicking above topmost line = top of screen
clicking below bottom-most line = bottom of screen
select text using mouse and shift
select text repeatedly using mouse and shift
cursor movement without shift resets selection
mouse click without shift resets selection
edit deletes selection
edit with shift key deletes selection
deleting selection may scroll
copy does not reset selection
cut
cut without selection
paste replaces selection
# search
search
search upwards
search wrap
search wrap upwards
# undo
undo insert text
undo delete text
undo restores selection

View File

@ -1,4 +1,6 @@
-- major tests for text editing flows -- major tests for text editing flows
-- Arguably this should be called edit_tests.lua,
-- but that would mess up the git blame at this point.
function test_initial_state() function test_initial_state()
App.screen.init{width=120, height=60} App.screen.init{width=120, height=60}
@ -802,6 +804,67 @@ function test_select_text_using_mouse()
check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos') check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')
end end
function test_select_text_using_mouse_starting_above_text()
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', 'def', 'xyz'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
Editor_state.selection1 = {}
edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 1, 'selection:pos')
end
function test_select_text_using_mouse_starting_above_text_wrapping_line()
-- first screen line starts in the middle of a line
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=2, pos=5}
Editor_state.screen_top1 = {line=2, pos=3}
Editor_state.screen_bottom1 = {}
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, Editor_state.left+8,5, 1)
-- selection is at screen top
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 2, 'selection:line')
check_eq(Editor_state.selection1.pos, 3, 'selection:pos')
end
function test_select_text_using_mouse_starting_below_text()
-- I'd like to test what happens when a mouse click is below some page of
-- text, potentially even in the middle of a line.
-- However, it's brittle to set up a text line boundary just right.
-- So I'm going to just check things below the bottom of the final line of
-- text when it's in the middle of the screen.
-- final screen line ends in the middle of screen
App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abcde'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'ab', 'baseline:screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'cde', 'baseline:screen:2')
-- press mouse above first line of text
edit.run_after_mouse_press(Editor_state, 5,App.screen.height-5, 1)
-- selection is past bottom-most text in screen
check(Editor_state.selection1.line ~= nil, 'selection:line-not-nil')
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 6, 'selection:pos')
end
function test_select_text_using_mouse_and_shift() function test_select_text_using_mouse_and_shift()
App.screen.init{width=50, height=60} App.screen.init{width=50, height=60}
Editor_state = edit.initialize_test_state() Editor_state = edit.initialize_test_state()
@ -856,6 +919,28 @@ function test_select_text_repeatedly_using_mouse_and_shift()
check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos') check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')
end end
function test_select_all_text()
-- display a single line of text
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc def'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
-- select all
App.fake_key_press('lctrl')
edit.run_after_keychord(Editor_state, 'C-a')
App.fake_key_release('lctrl')
edit.key_release(Editor_state, 'lctrl')
-- selection
check_eq(Editor_state.selection1.line, 1, 'selection:line')
check_eq(Editor_state.selection1.pos, 1, 'selection:pos')
check_eq(Editor_state.cursor1.line, 1, 'cursor:line')
check_eq(Editor_state.cursor1.pos, 8, 'cursor:pos')
end
function test_cut_without_selection() function test_cut_without_selection()
-- display a few lines -- display a few lines
App.screen.init{width=Editor_state.left+30, height=60} App.screen.init{width=Editor_state.left+30, height=60}