new fork: browse an immutable text file
Still has a cursor, which might be unnecessary.
This commit is contained in:
parent
08b7318c0d
commit
aeea740891
77
edit.lua
77
edit.lua
|
@ -65,11 +65,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c
|
|||
width = right-left,
|
||||
|
||||
filename = love.filesystem.getUserDirectory()..'/lines.txt',
|
||||
next_save = nil,
|
||||
|
||||
-- undo
|
||||
history = {},
|
||||
next_history = 1,
|
||||
|
||||
-- search
|
||||
search_term = nil,
|
||||
|
@ -111,23 +106,9 @@ function edit.draw(State)
|
|||
end
|
||||
|
||||
function edit.update(State, dt)
|
||||
if State.next_save and State.next_save < App.getTime() then
|
||||
save_to_disk(State)
|
||||
State.next_save = nil
|
||||
end
|
||||
end
|
||||
|
||||
function schedule_save(State)
|
||||
if State.next_save == nil then
|
||||
State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
|
||||
end
|
||||
end
|
||||
|
||||
function edit.quit(State)
|
||||
-- make sure to save before quitting
|
||||
if State.next_save then
|
||||
save_to_disk(State)
|
||||
end
|
||||
end
|
||||
|
||||
function edit.mouse_pressed(State, x,y, mouse_button)
|
||||
|
@ -193,10 +174,7 @@ function edit.textinput(State, t)
|
|||
State.search_term = State.search_term..t
|
||||
State.search_text = nil
|
||||
Text.search_next(State)
|
||||
else
|
||||
Text.textinput(State, t)
|
||||
end
|
||||
schedule_save(State)
|
||||
end
|
||||
|
||||
function edit.keychord_pressed(State, chord, key)
|
||||
|
@ -245,67 +223,12 @@ function edit.keychord_pressed(State, chord, key)
|
|||
elseif chord == 'C-0' then
|
||||
edit.update_font_settings(State, 20)
|
||||
Text.redraw_all(State)
|
||||
-- undo
|
||||
elseif chord == 'C-z' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local event = undo_event(State)
|
||||
if event then
|
||||
local src = event.before
|
||||
State.screen_top1 = deepcopy(src.screen_top)
|
||||
State.cursor1 = deepcopy(src.cursor)
|
||||
State.selection1 = deepcopy(src.selection)
|
||||
patch(State.lines, event.after, event.before)
|
||||
patch_placeholders(State.line_cache, event.after, event.before)
|
||||
-- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
Text.redraw_all(State)
|
||||
schedule_save(State)
|
||||
end
|
||||
elseif chord == 'C-y' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local event = redo_event(State)
|
||||
if event then
|
||||
local src = event.after
|
||||
State.screen_top1 = deepcopy(src.screen_top)
|
||||
State.cursor1 = deepcopy(src.cursor)
|
||||
State.selection1 = deepcopy(src.selection)
|
||||
patch(State.lines, event.before, event.after)
|
||||
-- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
Text.redraw_all(State)
|
||||
schedule_save(State)
|
||||
end
|
||||
-- clipboard
|
||||
elseif chord == 'C-c' then
|
||||
local s = Text.selection(State)
|
||||
if s then
|
||||
App.setClipboardText(s)
|
||||
end
|
||||
elseif chord == 'C-x' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
local s = Text.cut_selection(State, State.left, State.right)
|
||||
if s then
|
||||
App.setClipboardText(s)
|
||||
end
|
||||
schedule_save(State)
|
||||
elseif chord == 'C-v' then
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
-- We don't have a good sense of when to scroll, so we'll be conservative
|
||||
-- and sometimes scroll when we didn't quite need to.
|
||||
local before_line = State.cursor1.line
|
||||
local before = snapshot(State, before_line)
|
||||
local clipboard_data = App.getClipboardText()
|
||||
for _,code in utf8.codes(clipboard_data) do
|
||||
local c = utf8.char(code)
|
||||
if c == '\n' then
|
||||
Text.insert_return(State)
|
||||
else
|
||||
Text.insert_at_cursor(State, c)
|
||||
end
|
||||
end
|
||||
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, before_line, State.cursor1.line)})
|
||||
-- dispatch to text
|
||||
else
|
||||
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
|
||||
|
|
|
@ -44,33 +44,3 @@ function test_drop_file()
|
|||
check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')
|
||||
edit.draw(Editor_state)
|
||||
end
|
||||
|
||||
function test_drop_file_saves_previous()
|
||||
io.write('\ntest_drop_file_saves_previous')
|
||||
App.screen.init{width=Editor_state.left+300, height=300}
|
||||
-- initially editing a file called foo that hasn't been saved to filesystem yet
|
||||
Editor_state.lines = load_array{'abc', 'def'}
|
||||
Editor_state.filename = 'foo'
|
||||
schedule_save(Editor_state)
|
||||
-- now drag a new file bar from the filesystem
|
||||
App.filesystem['bar'] = 'abc\ndef\nghi\n'
|
||||
local fake_dropped_file = {
|
||||
opened = false,
|
||||
getFilename = function(self)
|
||||
return 'bar'
|
||||
end,
|
||||
open = function(self)
|
||||
self.opened = true
|
||||
end,
|
||||
lines = function(self)
|
||||
assert(self.opened)
|
||||
return App.filesystem['bar']:gmatch('[^\n]+')
|
||||
end,
|
||||
close = function(self)
|
||||
self.opened = false
|
||||
end,
|
||||
}
|
||||
App.filedropped(fake_dropped_file)
|
||||
-- filesystem now contains a file called foo
|
||||
check_eq(App.filesystem['foo'], 'abc\ndef\n', 'F - test_drop_file_saves_previous')
|
||||
end
|
||||
|
|
54
select.lua
54
select.lua
|
@ -95,60 +95,6 @@ function Text.to_pos(State, x,y)
|
|||
end
|
||||
end
|
||||
|
||||
function Text.cut_selection(State)
|
||||
if State.selection1.line == nil then return end
|
||||
local result = Text.selection(State)
|
||||
Text.delete_selection(State)
|
||||
return result
|
||||
end
|
||||
|
||||
function Text.delete_selection(State)
|
||||
if State.selection1.line == nil then return end
|
||||
local minl,maxl = minmax(State.selection1.line, State.cursor1.line)
|
||||
local before = snapshot(State, minl, maxl)
|
||||
Text.delete_selection_without_undo(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
|
||||
end
|
||||
|
||||
function Text.delete_selection_without_undo(State)
|
||||
if State.selection1.line == nil then return end
|
||||
-- min,max = sorted(State.selection1,State.cursor1)
|
||||
local minl,minp = State.selection1.line,State.selection1.pos
|
||||
local maxl,maxp = State.cursor1.line,State.cursor1.pos
|
||||
if minl > maxl then
|
||||
minl,maxl = maxl,minl
|
||||
minp,maxp = maxp,minp
|
||||
elseif minl == maxl then
|
||||
if minp > maxp then
|
||||
minp,maxp = maxp,minp
|
||||
end
|
||||
end
|
||||
-- update State.cursor1 and State.selection1
|
||||
State.cursor1.line = minl
|
||||
State.cursor1.pos = minp
|
||||
if Text.lt1(State.cursor1, State.screen_top1) then
|
||||
State.screen_top1.line = State.cursor1.line
|
||||
State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
|
||||
end
|
||||
State.selection1 = {}
|
||||
-- delete everything between min (inclusive) and max (exclusive)
|
||||
Text.clear_screen_line_cache(State, minl)
|
||||
local min_offset = Text.offset(State.lines[minl].data, minp)
|
||||
local max_offset = Text.offset(State.lines[maxl].data, maxp)
|
||||
if minl == maxl then
|
||||
--? print('minl == maxl')
|
||||
State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
|
||||
return
|
||||
end
|
||||
assert(minl < maxl)
|
||||
local rhs = State.lines[maxl].data:sub(max_offset)
|
||||
for i=maxl,minl+1,-1 do
|
||||
table.remove(State.lines, i)
|
||||
table.remove(State.line_cache, i)
|
||||
end
|
||||
State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs
|
||||
end
|
||||
|
||||
function Text.selection(State)
|
||||
if State.selection1.line == nil then return end
|
||||
-- min,max = sorted(State.selection1,State.cursor1)
|
||||
|
|
130
text.lua
130
text.lua
|
@ -3,7 +3,6 @@ Text = {}
|
|||
|
||||
require 'search'
|
||||
require 'select'
|
||||
require 'undo'
|
||||
require 'text_tests'
|
||||
|
||||
-- draw a line starting from startpos to screen at y between State.left and State.right
|
||||
|
@ -150,128 +149,11 @@ function Text.compute_fragments(State, line_index)
|
|||
end
|
||||
end
|
||||
|
||||
function Text.textinput(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
|
||||
local before = snapshot(State, State.cursor1.line)
|
||||
--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
|
||||
Text.insert_at_cursor(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
|
||||
|
||||
function Text.insert_at_cursor(State, t)
|
||||
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 love.textinput above.
|
||||
function Text.keychord_pressed(State, chord)
|
||||
--? print('chord', chord, State.selection1.line, State.selection1.pos)
|
||||
--== shortcuts that mutate text
|
||||
if chord == 'return' then
|
||||
local before_line = State.cursor1.line
|
||||
local before = snapshot(State, before_line)
|
||||
Text.insert_return(State)
|
||||
State.selection1 = {}
|
||||
if State.cursor_y > App.screen.height - State.line_height then
|
||||
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
|
||||
end
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
|
||||
elseif chord == 'tab' then
|
||||
local before = snapshot(State, State.cursor1.line)
|
||||
--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
|
||||
Text.insert_at_cursor(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
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
|
||||
elseif chord == 'backspace' then
|
||||
if State.selection1.line then
|
||||
Text.delete_selection(State, State.left, State.right)
|
||||
schedule_save(State)
|
||||
return
|
||||
end
|
||||
local before
|
||||
if State.cursor1.pos > 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)
|
||||
if byte_start then
|
||||
if byte_end then
|
||||
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
|
||||
else
|
||||
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
|
||||
end
|
||||
State.cursor1.pos = State.cursor1.pos-1
|
||||
end
|
||||
elseif State.cursor1.line > 1 then
|
||||
before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
|
||||
-- join lines
|
||||
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
|
||||
State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
|
||||
table.remove(State.lines, State.cursor1.line)
|
||||
table.remove(State.line_cache, State.cursor1.line)
|
||||
State.cursor1.line = State.cursor1.line-1
|
||||
end
|
||||
if State.screen_top1.line > #State.lines then
|
||||
Text.populate_screen_line_starting_pos(State, #State.lines)
|
||||
local line_cache = State.line_cache[#State.line_cache]
|
||||
State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
|
||||
elseif Text.lt1(State.cursor1, State.screen_top1) then
|
||||
local top2 = Text.to2(State, State.screen_top1)
|
||||
top2 = Text.previous_screen_line(State, top2, State.left, State.right)
|
||||
State.screen_top1 = Text.to1(State, top2)
|
||||
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
|
||||
end
|
||||
Text.clear_screen_line_cache(State, State.cursor1.line)
|
||||
assert(Text.le1(State.screen_top1, State.cursor1))
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
|
||||
elseif chord == 'delete' then
|
||||
if State.selection1.line then
|
||||
Text.delete_selection(State, State.left, State.right)
|
||||
schedule_save(State)
|
||||
return
|
||||
end
|
||||
local before
|
||||
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
|
||||
before = snapshot(State, State.cursor1.line)
|
||||
else
|
||||
before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
|
||||
end
|
||||
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
|
||||
local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
|
||||
local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
|
||||
if byte_start then
|
||||
if byte_end then
|
||||
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
|
||||
else
|
||||
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
|
||||
end
|
||||
-- no change to State.cursor1.pos
|
||||
end
|
||||
elseif State.cursor1.line < #State.lines then
|
||||
-- join lines
|
||||
State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
|
||||
table.remove(State.lines, State.cursor1.line+1)
|
||||
table.remove(State.line_cache, State.cursor1.line+1)
|
||||
end
|
||||
Text.clear_screen_line_cache(State, State.cursor1.line)
|
||||
schedule_save(State)
|
||||
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
|
||||
--== shortcuts that move the cursor
|
||||
elseif chord == 'left' then
|
||||
if chord == 'left' then
|
||||
Text.left(State)
|
||||
State.selection1 = {}
|
||||
elseif chord == 'right' then
|
||||
|
@ -355,16 +237,6 @@ function Text.keychord_pressed(State, chord)
|
|||
end
|
||||
end
|
||||
|
||||
function Text.insert_return(State)
|
||||
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
|
||||
table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
|
||||
table.insert(State.line_cache, State.cursor1.line+1, {})
|
||||
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
|
||||
Text.clear_screen_line_cache(State, State.cursor1.line)
|
||||
State.cursor1.line = State.cursor1.line+1
|
||||
State.cursor1.pos = 1
|
||||
end
|
||||
|
||||
function Text.pageup(State)
|
||||
--? print('pageup')
|
||||
-- duplicate some logic from love.draw
|
||||
|
|
675
text_tests.lua
675
text_tests.lua
|
@ -14,47 +14,6 @@ function test_initial_state()
|
|||
check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos')
|
||||
end
|
||||
|
||||
function test_backspace_from_start_of_final_line()
|
||||
io.write('\ntest_backspace_from_start_of_final_line')
|
||||
-- display final line of text with cursor at start of it
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def'}
|
||||
Editor_state.screen_top1 = {line=2, pos=1}
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
Text.redraw_all(Editor_state)
|
||||
-- backspace scrolls up
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(#Editor_state.lines, 1, 'F - test_backspace_from_start_of_final_line/#lines')
|
||||
check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_from_start_of_final_line/cursor')
|
||||
check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_from_start_of_final_line/screen_top')
|
||||
end
|
||||
|
||||
function test_insert_first_character()
|
||||
io.write('\ntest_insert_first_character')
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{}
|
||||
Text.redraw_all(Editor_state)
|
||||
edit.draw(Editor_state)
|
||||
edit.run_after_textinput(Editor_state, 'a')
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'a', 'F - test_insert_first_character/screen:1')
|
||||
end
|
||||
|
||||
function test_press_ctrl()
|
||||
io.write('\ntest_press_ctrl')
|
||||
-- press ctrl while the cursor is on text
|
||||
App.screen.init{width=50, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{''}
|
||||
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.run_after_keychord(Editor_state, 'C-m')
|
||||
end
|
||||
|
||||
function test_move_left()
|
||||
io.write('\ntest_move_left')
|
||||
App.screen.init{width=120, height=60}
|
||||
|
@ -604,47 +563,6 @@ function test_cursor_movement_without_shift_resets_selection()
|
|||
check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')
|
||||
end
|
||||
|
||||
function test_edit_deletes_selection()
|
||||
io.write('\ntest_edit_deletes_selection')
|
||||
-- display a line of text with some part selected
|
||||
App.screen.init{width=75, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- press a key
|
||||
edit.run_after_textinput(Editor_state, 'x')
|
||||
-- selected text is deleted and replaced with the key
|
||||
check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')
|
||||
end
|
||||
|
||||
function test_edit_with_shift_key_deletes_selection()
|
||||
io.write('\ntest_edit_with_shift_key_deletes_selection')
|
||||
-- display a line of text with some part selected
|
||||
App.screen.init{width=75, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- mimic precise keypresses for a capital letter
|
||||
App.fake_key_press('lshift')
|
||||
edit.keychord_pressed(Editor_state, 'd', 'd')
|
||||
edit.textinput(Editor_state, 'D')
|
||||
edit.key_released(Editor_state, 'd')
|
||||
App.fake_key_release('lshift')
|
||||
-- selected text is deleted and replaced with the key
|
||||
check_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection')
|
||||
check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')
|
||||
end
|
||||
|
||||
function test_copy_does_not_reset_selection()
|
||||
io.write('\ntest_copy_does_not_reset_selection')
|
||||
-- display a line of text with a selection
|
||||
|
@ -664,170 +582,6 @@ function test_copy_does_not_reset_selection()
|
|||
check(Editor_state.selection1.line, 'F - test_copy_does_not_reset_selection')
|
||||
end
|
||||
|
||||
function test_cut()
|
||||
io.write('\ntest_cut')
|
||||
-- display a line of text with some part selected
|
||||
App.screen.init{width=75, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- press a key
|
||||
edit.run_after_keychord(Editor_state, 'C-x')
|
||||
check_eq(App.clipboard, 'a', 'F - test_cut/clipboard')
|
||||
-- selected text is deleted
|
||||
check_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')
|
||||
end
|
||||
|
||||
function test_paste_replaces_selection()
|
||||
io.write('\ntest_paste_replaces_selection')
|
||||
-- display a line of text with a selection
|
||||
App.screen.init{width=75, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=1}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- set clipboard
|
||||
App.clipboard = 'xyz'
|
||||
-- paste selection
|
||||
edit.run_after_keychord(Editor_state, 'C-v')
|
||||
-- selection is reset since shift key is not pressed
|
||||
-- selection includes the newline, so it's also deleted
|
||||
check_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection')
|
||||
end
|
||||
|
||||
function test_deleting_selection_may_scroll()
|
||||
io.write('\ntest_deleting_selection_may_scroll')
|
||||
-- display lines 2/3/4
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=3, pos=2}
|
||||
Editor_state.screen_top1 = {line=2, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'def', 'F - test_deleting_selection_may_scroll/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_deleting_selection_may_scroll/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'jkl', 'F - test_deleting_selection_may_scroll/baseline/screen:3')
|
||||
-- set up a selection starting above the currently displayed page
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
-- delete selection
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
-- page scrolls up
|
||||
check_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll')
|
||||
check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')
|
||||
end
|
||||
|
||||
function test_edit_wrapping_text()
|
||||
io.write('\ntest_edit_wrapping_text')
|
||||
App.screen.init{width=50, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'xyz'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=4}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
edit.run_after_textinput(Editor_state, 'g')
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')
|
||||
end
|
||||
|
||||
function test_insert_newline()
|
||||
io.write('\ntest_insert_newline')
|
||||
-- display a few lines
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')
|
||||
-- hitting the enter key splits the line
|
||||
edit.run_after_keychord(Editor_state, 'return')
|
||||
check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_newline/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'a', 'F - test_insert_newline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_insert_newline/screen:3')
|
||||
end
|
||||
|
||||
function test_insert_newline_at_start_of_line()
|
||||
io.write('\ntest_insert_newline_at_start_of_line')
|
||||
-- display a line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
-- hitting the enter key splits the line
|
||||
edit.run_after_keychord(Editor_state, 'return')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos')
|
||||
check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1')
|
||||
check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2')
|
||||
end
|
||||
|
||||
function test_insert_from_clipboard()
|
||||
io.write('\ntest_insert_from_clipboard')
|
||||
-- display a few lines
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')
|
||||
-- paste some text including a newline, check that new line is created
|
||||
App.clipboard = 'xy\nz'
|
||||
edit.run_after_keychord(Editor_state, 'C-v')
|
||||
check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_from_clipboard/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_from_clipboard/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 2, 'F - test_insert_from_clipboard/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3')
|
||||
end
|
||||
|
||||
function test_move_cursor_using_mouse()
|
||||
io.write('\ntest_move_cursor_using_mouse')
|
||||
App.screen.init{width=50, height=60}
|
||||
|
@ -923,24 +677,6 @@ function test_select_text_repeatedly_using_mouse_and_shift()
|
|||
check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:pos')
|
||||
end
|
||||
|
||||
function test_cut_without_selection()
|
||||
io.write('\ntest_cut_without_selection')
|
||||
-- display a few lines
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
Editor_state.selection1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- try to cut without selecting text
|
||||
edit.run_after_keychord(Editor_state, 'C-x')
|
||||
-- no crash
|
||||
check_nil(Editor_state.selection1.line, 'F - test_cut_without_selection')
|
||||
end
|
||||
|
||||
function test_pagedown()
|
||||
io.write('\ntest_pagedown')
|
||||
App.screen.init{width=120, height=45}
|
||||
|
@ -1427,112 +1163,6 @@ function test_pageup_scrolls_up_from_middle_screen_line()
|
|||
App.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3')
|
||||
end
|
||||
|
||||
function test_enter_on_bottom_line_scrolls_down()
|
||||
io.write('\ntest_enter_on_bottom_line_scrolls_down')
|
||||
-- display a few lines with cursor on bottom line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=3, pos=2}
|
||||
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, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')
|
||||
-- after hitting the enter key the screen scrolls down
|
||||
edit.run_after_keychord(Editor_state, 'return')
|
||||
check_eq(Editor_state.screen_top1.line, 2, 'F - test_enter_on_bottom_line_scrolls_down/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 4, 'F - test_enter_on_bottom_line_scrolls_down/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_bottom_line_scrolls_down/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3')
|
||||
end
|
||||
|
||||
function test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()
|
||||
io.write('\ntest_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom')
|
||||
-- display just the bottom line on screen
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=4, pos=2}
|
||||
Editor_state.screen_top1 = {line=4, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'jkl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/baseline/screen:1')
|
||||
-- after hitting the enter key the screen does not scroll down
|
||||
edit.run_after_keychord(Editor_state, 'return')
|
||||
check_eq(Editor_state.screen_top1.line, 4, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 5, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')
|
||||
end
|
||||
|
||||
function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()
|
||||
io.write('\ntest_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom')
|
||||
-- display just an empty bottom line on screen
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', ''}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
Editor_state.screen_top1 = {line=2, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- after hitting the inserting_text key the screen does not scroll down
|
||||
edit.run_after_textinput(Editor_state, 'a')
|
||||
check_eq(Editor_state.screen_top1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
|
||||
end
|
||||
|
||||
function test_typing_on_bottom_line_scrolls_down()
|
||||
io.write('\ntest_typing_on_bottom_line_scrolls_down')
|
||||
-- display a few lines with cursor on bottom line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=3, pos=4}
|
||||
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, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:3')
|
||||
-- after typing something the line wraps and the screen scrolls down
|
||||
edit.run_after_textinput(Editor_state, 'j')
|
||||
edit.run_after_textinput(Editor_state, 'k')
|
||||
edit.run_after_textinput(Editor_state, 'l')
|
||||
check_eq(Editor_state.screen_top1.line, 2, 'F - test_typing_on_bottom_line_scrolls_down/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 3, 'F - test_typing_on_bottom_line_scrolls_down/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 7, 'F - test_typing_on_bottom_line_scrolls_down/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')
|
||||
end
|
||||
|
||||
function test_left_arrow_scrolls_up_in_wrapped_line()
|
||||
io.write('\ntest_left_arrow_scrolls_up_in_wrapped_line')
|
||||
-- display lines starting from second screen line of a line
|
||||
|
@ -1655,311 +1285,6 @@ function test_end_scrolls_down_in_wrapped_line()
|
|||
App.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')
|
||||
end
|
||||
|
||||
function test_position_cursor_on_recently_edited_wrapping_line()
|
||||
-- draw a line wrapping over 2 screen lines
|
||||
io.write('\ntest_position_cursor_on_recently_edited_wrapping_line')
|
||||
App.screen.init{width=100, height=200}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=25}
|
||||
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, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'xyz', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:3')
|
||||
-- add to the line until it's wrapping over 3 screen lines
|
||||
edit.run_after_textinput(Editor_state, 's')
|
||||
edit.run_after_textinput(Editor_state, 't')
|
||||
edit.run_after_textinput(Editor_state, 'u')
|
||||
check_eq(Editor_state.cursor1.pos, 28, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3')
|
||||
-- try to move the cursor earlier in the third screen line by clicking the mouse
|
||||
edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)
|
||||
-- cursor should move
|
||||
check_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')
|
||||
end
|
||||
|
||||
function test_backspace_can_scroll_up()
|
||||
io.write('\ntest_backspace_can_scroll_up')
|
||||
-- display the lines 2/3/4 with the cursor on line 2
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
Editor_state.screen_top1 = {line=2, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')
|
||||
-- after hitting backspace the screen scrolls up by one line
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_can_scroll_up/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_can_scroll_up/cursor')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3')
|
||||
end
|
||||
|
||||
function test_backspace_can_scroll_up_screen_line()
|
||||
io.write('\ntest_backspace_can_scroll_up_screen_line')
|
||||
-- display lines starting from second screen line of a line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=3, pos=5}
|
||||
Editor_state.screen_top1 = {line=3, pos=5}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:2')
|
||||
-- after hitting backspace the screen scrolls up by one screen line
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3')
|
||||
check_eq(Editor_state.screen_top1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/screen_top')
|
||||
check_eq(Editor_state.screen_top1.pos, 1, 'F - test_backspace_can_scroll_up_screen_line/screen_top')
|
||||
check_eq(Editor_state.cursor1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 4, 'F - test_backspace_can_scroll_up_screen_line/cursor:pos')
|
||||
end
|
||||
|
||||
function test_backspace_past_line_boundary()
|
||||
io.write('\ntest_backspace_past_line_boundary')
|
||||
-- position cursor at start of a (non-first) line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
-- backspace joins with previous line
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")
|
||||
end
|
||||
|
||||
-- some tests for operating over selections created using Shift- chords
|
||||
-- we're just testing delete_selection, and it works the same for all keys
|
||||
|
||||
function test_backspace_over_selection()
|
||||
io.write('\ntest_backspace_over_selection')
|
||||
-- select just one character within a line with cursor before selection
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
-- backspace deletes the selected character, even though it's after the cursor
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data")
|
||||
-- cursor (remains) at start of selection
|
||||
check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection/cursor:line")
|
||||
check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos")
|
||||
-- selection is cleared
|
||||
check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection/selection")
|
||||
end
|
||||
|
||||
function test_backspace_over_selection_reverse()
|
||||
io.write('\ntest_backspace_over_selection_reverse')
|
||||
-- select just one character within a line with cursor after selection
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.selection1 = {line=1, pos=1}
|
||||
-- backspace deletes the selected character
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data")
|
||||
-- cursor moves to start of selection
|
||||
check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection_reverse/cursor:line")
|
||||
check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection_reverse/cursor:pos")
|
||||
-- selection is cleared
|
||||
check_nil(Editor_state.selection1.line, "F - test_backspace_over_selection_reverse/selection")
|
||||
end
|
||||
|
||||
function test_backspace_over_multiple_lines()
|
||||
io.write('\ntest_backspace_over_multiple_lines')
|
||||
-- select just one character within a line with cursor after selection
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.selection1 = {line=4, pos=2}
|
||||
-- backspace deletes the region and joins the remaining portions of lines on either side
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1")
|
||||
check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2")
|
||||
-- cursor remains at start of selection
|
||||
check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_multiple_lines/cursor:line")
|
||||
check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_over_multiple_lines/cursor:pos")
|
||||
-- selection is cleared
|
||||
check_nil(Editor_state.selection1.line, "F - test_backspace_over_multiple_lines/selection")
|
||||
end
|
||||
|
||||
function test_backspace_to_end_of_line()
|
||||
io.write('\ntest_backspace_to_end_of_line')
|
||||
-- select region from cursor to end of line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=2}
|
||||
Editor_state.selection1 = {line=1, pos=4}
|
||||
-- backspace deletes rest of line without joining to any other line
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1")
|
||||
check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2")
|
||||
-- cursor remains at start of selection
|
||||
check_eq(Editor_state.cursor1.line, 1, "F - test_backspace_to_start_of_line/cursor:line")
|
||||
check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_to_start_of_line/cursor:pos")
|
||||
-- selection is cleared
|
||||
check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")
|
||||
end
|
||||
|
||||
function test_backspace_to_start_of_line()
|
||||
io.write('\ntest_backspace_to_start_of_line')
|
||||
-- select region from cursor to start of line
|
||||
App.screen.init{width=Editor_state.left+30, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=1}
|
||||
Editor_state.selection1 = {line=2, pos=3}
|
||||
-- backspace deletes beginning of line without joining to any other line
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1")
|
||||
check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2")
|
||||
-- cursor remains at start of selection
|
||||
check_eq(Editor_state.cursor1.line, 2, "F - test_backspace_to_start_of_line/cursor:line")
|
||||
check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_to_start_of_line/cursor:pos")
|
||||
-- selection is cleared
|
||||
check_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")
|
||||
end
|
||||
|
||||
function test_undo_insert_text()
|
||||
io.write('\ntest_undo_insert_text')
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'def', 'xyz'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=4}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
-- insert a character
|
||||
edit.draw(Editor_state)
|
||||
edit.run_after_textinput(Editor_state, 'g')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos')
|
||||
check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/baseline/selection:line')
|
||||
check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/baseline/selection:pos')
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')
|
||||
-- undo
|
||||
edit.run_after_keychord(Editor_state, 'C-z')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos')
|
||||
check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/selection:line')
|
||||
check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/selection:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')
|
||||
end
|
||||
|
||||
function test_undo_delete_text()
|
||||
io.write('\ntest_undo_delete_text')
|
||||
App.screen.init{width=120, height=60}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc', 'defg', 'xyz'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=2, pos=5}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
-- delete a character
|
||||
edit.run_after_keychord(Editor_state, 'backspace')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos')
|
||||
check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/baseline/selection:line')
|
||||
check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/baseline/selection:pos')
|
||||
local y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')
|
||||
-- undo
|
||||
--? -- after undo, the backspaced key is selected
|
||||
edit.run_after_keychord(Editor_state, 'C-z')
|
||||
check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/cursor:line')
|
||||
check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos')
|
||||
check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/selection:line')
|
||||
check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/selection:pos')
|
||||
--? check_eq(Editor_state.selection1.line, 2, 'F - test_undo_delete_text/selection:line')
|
||||
--? check_eq(Editor_state.selection1.pos, 4, 'F - test_undo_delete_text/selection:pos')
|
||||
y = Editor_state.top
|
||||
App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')
|
||||
y = y + Editor_state.line_height
|
||||
App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
|
||||
end
|
||||
|
||||
function test_undo_restores_selection()
|
||||
io.write('\ntest_undo_restores_selection')
|
||||
-- display a line of text with some part selected
|
||||
App.screen.init{width=75, height=80}
|
||||
Editor_state = edit.initialize_test_state()
|
||||
Editor_state.lines = load_array{'abc'}
|
||||
Text.redraw_all(Editor_state)
|
||||
Editor_state.cursor1 = {line=1, pos=1}
|
||||
Editor_state.selection1 = {line=1, pos=2}
|
||||
Editor_state.screen_top1 = {line=1, pos=1}
|
||||
Editor_state.screen_bottom1 = {}
|
||||
edit.draw(Editor_state)
|
||||
-- delete selected text
|
||||
edit.run_after_textinput(Editor_state, 'x')
|
||||
check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline')
|
||||
check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection')
|
||||
-- undo
|
||||
edit.run_after_keychord(Editor_state, 'C-z')
|
||||
edit.run_after_keychord(Editor_state, 'C-z')
|
||||
-- selection is restored
|
||||
check_eq(Editor_state.selection1.line, 1, 'F - test_undo_restores_selection/line')
|
||||
check_eq(Editor_state.selection1.pos, 2, 'F - test_undo_restores_selection/pos')
|
||||
end
|
||||
|
||||
function test_search()
|
||||
io.write('\ntest_search')
|
||||
App.screen.init{width=120, height=60}
|
||||
|
|
110
undo.lua
110
undo.lua
|
@ -1,110 +0,0 @@
|
|||
-- undo/redo by managing the sequence of events in the current session
|
||||
-- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu
|
||||
|
||||
-- Incredibly inefficient; we make a copy of lines on every single keystroke.
|
||||
-- The hope here is that we're either editing small files or just reading large files.
|
||||
-- TODO: highlight stuff inserted by any undo/redo operation
|
||||
-- TODO: coalesce multiple similar operations
|
||||
|
||||
function record_undo_event(State, data)
|
||||
State.history[State.next_history] = data
|
||||
State.next_history = State.next_history+1
|
||||
for i=State.next_history,#State.history do
|
||||
State.history[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function undo_event(State)
|
||||
if State.next_history > 1 then
|
||||
--? print('moving to history', State.next_history-1)
|
||||
State.next_history = State.next_history-1
|
||||
local result = State.history[State.next_history]
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function redo_event(State)
|
||||
if State.next_history <= #State.history then
|
||||
--? print('restoring history', State.next_history+1)
|
||||
local result = State.history[State.next_history]
|
||||
State.next_history = State.next_history+1
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Copy all relevant global state.
|
||||
-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
|
||||
function snapshot(State, s,e)
|
||||
-- Snapshot everything by default, but subset if requested.
|
||||
assert(s)
|
||||
if e == nil then
|
||||
e = s
|
||||
end
|
||||
assert(#State.lines > 0)
|
||||
if s < 1 then s = 1 end
|
||||
if s > #State.lines then s = #State.lines end
|
||||
if e < 1 then e = 1 end
|
||||
if e > #State.lines then e = #State.lines end
|
||||
-- compare with App.initialize_globals
|
||||
local event = {
|
||||
screen_top=deepcopy(State.screen_top1),
|
||||
selection=deepcopy(State.selection1),
|
||||
cursor=deepcopy(State.cursor1),
|
||||
lines={},
|
||||
start_line=s,
|
||||
end_line=e,
|
||||
-- no filename; undo history is cleared when filename changes
|
||||
}
|
||||
-- deep copy lines without cached stuff like text fragments
|
||||
for i=s,e do
|
||||
local line = State.lines[i]
|
||||
table.insert(event.lines, {data=line.data})
|
||||
end
|
||||
return event
|
||||
end
|
||||
|
||||
function patch(lines, from, to)
|
||||
--? if #from.lines == 1 and #to.lines == 1 then
|
||||
--? assert(from.start_line == from.end_line)
|
||||
--? assert(to.start_line == to.end_line)
|
||||
--? assert(from.start_line == to.start_line)
|
||||
--? lines[from.start_line] = to.lines[1]
|
||||
--? return
|
||||
--? end
|
||||
assert(from.start_line == to.start_line)
|
||||
for i=from.end_line,from.start_line,-1 do
|
||||
table.remove(lines, i)
|
||||
end
|
||||
assert(#to.lines == to.end_line-to.start_line+1)
|
||||
for i=1,#to.lines do
|
||||
table.insert(lines, to.start_line+i-1, to.lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
function patch_placeholders(line_cache, from, to)
|
||||
assert(from.start_line == to.start_line)
|
||||
for i=from.end_line,from.start_line,-1 do
|
||||
table.remove(line_cache, i)
|
||||
end
|
||||
assert(#to.lines == to.end_line-to.start_line+1)
|
||||
for i=1,#to.lines do
|
||||
table.insert(line_cache, to.start_line+i-1, {})
|
||||
end
|
||||
end
|
||||
|
||||
-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
|
||||
function deepcopy(obj, seen)
|
||||
if type(obj) ~= 'table' then return obj end
|
||||
if seen and seen[obj] then return seen[obj] end
|
||||
local s = seen or {}
|
||||
local result = setmetatable({}, getmetatable(obj))
|
||||
s[obj] = result
|
||||
for k,v in pairs(obj) do
|
||||
result[deepcopy(k, s)] = deepcopy(v, s)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function minmax(a, b)
|
||||
return math.min(a,b), math.max(a,b)
|
||||
end
|
Loading…
Reference in New Issue