bring back everything from commit a68647ae22

We only want to unwind the source editing.
This commit is contained in:
Kartik K. Agaram 2022-11-06 08:53:46 -08:00
parent ee18ce96a1
commit b64a60a509
9 changed files with 120 additions and 43 deletions

View File

@ -3,6 +3,9 @@ program before it ever runs. However, some things don't have tests yet, either
because I don't know how to test them or because I've been lazy. I'll at least
record those here.
Startup:
- terminal log shows unit tests running
* Initializing settings:
- from previous session
- Filename as absolute path

42
app.lua
View File

@ -11,6 +11,7 @@
--
-- Scroll below this function for more details.
function love.run()
App.snapshot_love()
-- Tests always run at the start.
App.run_tests()
@ -123,6 +124,18 @@ end
App = {screen={}}
-- save/restore various framework globals we care about -- only on very first load
function App.snapshot_love()
if Love_snapshot then return end
Love_snapshot = {}
-- save the entire initial font; it doesn't seem reliably recreated using newFont
Love_snapshot.initial_font = love.graphics.getFont()
end
function App.undo_initialize()
love.graphics.setFont(Love_snapshot.initial_font)
end
function App.initialize_for_test()
App.screen.init({width=100, height=50})
App.screen.contents = {} -- clear screen
@ -137,6 +150,27 @@ function App.screen.init(dims)
App.screen.height = dims.height
end
-- operations on the LÖVE window within the monitor/display
function App.screen.resize(width, height, flags)
App.screen.width = width
App.screen.height = height
App.screen.flags = flags
end
function App.screen.size()
return App.screen.width, App.screen.height, App.screen.flags
end
function App.screen.move(x,y, displayindex)
App.screen.x = x
App.screen.y = y
App.screen.displayindex = displayindex
end
function App.screen.position()
return App.screen.x, App.screen.y, App.screen.displayindex
end
function App.screen.print(msg, x,y)
local screen_row = 'y'..tostring(y)
--? print('drawing "'..msg..'" at y '..tostring(y))
@ -156,6 +190,10 @@ function App.color(color)
love.graphics.setColor(color.r, color.g, color.b, color.a)
end
function colortable(app_color)
return {app_color.r, app_color.g, app_color.b, app_color.a}
end
App.time = 1
function App.getTime()
return App.time
@ -362,6 +400,10 @@ function App.disable_tests()
App.fake_mouse_press = nil
App.fake_mouse_release = nil
-- other methods dispatch to real hardware
App.screen.resize = love.window.setMode
App.screen.size = love.window.getMode
App.screen.move = love.window.setPosition
App.screen.position = love.window.getPosition
App.screen.print = love.graphics.print
App.newText = love.graphics.newText
App.screen.draw = love.graphics.draw

View File

@ -64,7 +64,7 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c
right = right,
width = right-left,
filename = love.filesystem.getUserDirectory()..'/lines.txt',
filename = love.filesystem.getUserDirectory()..'/lines.txt', -- '/' should work even on Windows
next_save = nil,
-- undo
@ -94,7 +94,7 @@ function edit.draw(State)
local line = State.lines[line_index]
--? print('draw:', y, line_index, line)
if y + State.line_height > App.screen.height then break end
State.screen_bottom1.line = line_index
State.screen_bottom1 = {line=line_index, pos=nil}
--? print('text.draw', y, line_index)
local startpos = 1
if line_index == State.screen_top1.line then
@ -231,7 +231,10 @@ function edit.keychord_pressed(State, chord, key)
return
elseif chord == 'C-f' then
State.search_term = ''
State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos}, screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos}}
State.search_backup = {
cursor={line=State.cursor1.line, pos=State.cursor1.pos},
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
}
assert(State.search_text == nil)
-- zoom
elseif chord == 'C-=' then
@ -324,14 +327,17 @@ end
--== some methods for tests
-- Insulate tests from some key globals so I don't have to change the vast
-- majority of tests when they're modified for the real app.
Test_margin_left = 25
Test_margin_right = 0
function edit.initialize_test_state()
-- if you change these values, tests will start failing
return edit.initialize_state(
15, -- top margin
Test_margin_left,
App.screen.width, -- right margin = 0
App.screen.width - Test_margin_right,
14, -- font height assuming default LÖVE font
15) -- line height
end

View File

@ -37,7 +37,8 @@ function save_to_disk(State)
error('failed to write to "'..State.filename..'"')
end
for _,line in ipairs(State.lines) do
outfile:write(line.data, '\n')
outfile:write(line.data)
outfile:write('\n')
end
outfile:close()
end
@ -57,3 +58,21 @@ function load_array(a)
end
return result
end
function is_absolute_path(path)
local os_path_separator = package.config:sub(1,1)
if os_path_separator == '/' then
-- POSIX systems permit backslashes in filenames
return path:sub(1,1) == '/'
elseif os_path_separator == '\\' then
if path:sub(2,2) == ':' then return true end -- DOS drive letter followed by volume separator
local f = path:sub(1,1)
return f == '/' or f == '\\'
else
error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')
end
end
function is_relative_path(path)
return not is_absolute_path(path)
end

View File

@ -56,9 +56,17 @@ end
array = {}
function array.find(arr, elem)
for i,x in ipairs(arr) do
if x == elem then
return i
if type(elem) == 'function' then
for i,x in ipairs(arr) do
if elem(x) then
return i
end
end
else
for i,x in ipairs(arr) do
if x == elem then
return i
end
end
end
return nil

View File

@ -70,12 +70,12 @@ function load_settings()
-- maximize window to determine maximum allowable dimensions
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
-- set up desired window dimensions
love.window.setPosition(settings.x, settings.y, settings.displayindex)
App.screen.flags.resizable = true
App.screen.flags.minwidth = math.min(App.screen.width, 200)
App.screen.flags.minheight = math.min(App.screen.width, 200)
App.screen.width, App.screen.height = settings.width, settings.height
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
love.window.setPosition(settings.x, settings.y, settings.displayindex)
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, settings.font_height, math.floor(settings.font_height*1.3))
Editor_state.filename = settings.filename
Editor_state.screen_top1 = settings.screen_top
@ -151,7 +151,7 @@ function love.quit()
-- save some important settings
local x,y,displayindex = love.window.getPosition()
local filename = Editor_state.filename
if filename:sub(1,1) ~= '/' then
if is_relative_path(filename) then
filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
end
local settings = {

View File

@ -6,13 +6,14 @@ function test_resize_window()
check_eq(App.screen.width, 300, 'F - test_resize_window/baseline/width')
check_eq(App.screen.height, 300, 'F - test_resize_window/baseline/height')
check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/baseline/left_margin')
check_eq(Editor_state.right, 300 - Test_margin_right, 'F - test_resize_window/baseline/left_margin')
App.resize(200, 400)
-- ugly; resize switches to real, non-test margins
check_eq(App.screen.width, 200, 'F - test_resize_window/width')
check_eq(App.screen.height, 400, 'F - test_resize_window/height')
check_eq(Editor_state.left, Test_margin_left, 'F - test_resize_window/left_margin')
-- ugly; right margin switches from 0 after resize
check_eq(Editor_state.left, Margin_left, 'F - test_resize_window/left_margin')
check_eq(Editor_state.right, 200-Margin_right, 'F - test_resize_window/right_margin')
check_eq(Editor_state.width, 200-Test_margin_left-Margin_right, 'F - test_resize_window/drawing_width')
check_eq(Editor_state.width, 200-Margin_left-Margin_right, 'F - test_resize_window/drawing_width')
-- TODO: how to make assertions about when App.update got past the early exit?
end

View File

@ -21,17 +21,16 @@ end
function Text.search_next(State)
-- search current line from cursor
local pos = find(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos)
local pos = find(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos, --[[literal]] true)
if pos then
State.cursor1.pos = pos
end
if pos == nil then
-- search lines below cursor
for i=State.cursor1.line+1,#State.lines do
pos = find(State.lines[i].data, State.search_term)
pos = find(State.lines[i].data, State.search_term, --[[from start]] nil, --[[literal]] true)
if pos then
State.cursor1.line = i
State.cursor1.pos = pos
State.cursor1 = {line=i, pos=pos}
break
end
end
@ -39,17 +38,16 @@ function Text.search_next(State)
if pos == nil then
-- wrap around
for i=1,State.cursor1.line-1 do
pos = find(State.lines[i].data, State.search_term)
pos = find(State.lines[i].data, State.search_term, --[[from start]] nil, --[[literal]] true)
if pos then
State.cursor1.line = i
State.cursor1.pos = pos
State.cursor1 = {line=i, pos=pos}
break
end
end
end
if pos == nil then
-- search current line until cursor
pos = find(State.lines[State.cursor1.line].data, State.search_term)
pos = find(State.lines[State.cursor1.line].data, State.search_term, --[[from start]] nil, --[[literal]] true)
if pos and pos < State.cursor1.pos then
State.cursor1.pos = pos
end
@ -69,17 +67,16 @@ end
function Text.search_previous(State)
-- search current line before cursor
local pos = rfind(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos-1)
local pos = rfind(State.lines[State.cursor1.line].data, State.search_term, State.cursor1.pos-1, --[[literal]] true)
if pos then
State.cursor1.pos = pos
end
if pos == nil then
-- search lines above cursor
for i=State.cursor1.line-1,1,-1 do
pos = rfind(State.lines[i].data, State.search_term)
pos = rfind(State.lines[i].data, State.search_term, --[[from end]] nil, --[[literal]] true)
if pos then
State.cursor1.line = i
State.cursor1.pos = pos
State.cursor1 = {line=i, pos=pos}
break
end
end
@ -87,17 +84,16 @@ function Text.search_previous(State)
if pos == nil then
-- wrap around
for i=#State.lines,State.cursor1.line+1,-1 do
pos = rfind(State.lines[i].data, State.search_term)
pos = rfind(State.lines[i].data, State.search_term, --[[from end]] nil, --[[literal]] true)
if pos then
State.cursor1.line = i
State.cursor1.pos = pos
State.cursor1 = {line=i, pos=pos}
break
end
end
end
if pos == nil then
-- search current line after cursor
pos = rfind(State.lines[State.cursor1.line].data, State.search_term)
pos = rfind(State.lines[State.cursor1.line].data, State.search_term, --[[from end]] nil, --[[literal]] true)
if pos and pos > State.cursor1.pos then
State.cursor1.pos = pos
end
@ -115,18 +111,18 @@ function Text.search_previous(State)
end
end
function find(s, pat, i)
function find(s, pat, i, plain)
if s == nil then return end
return s:find(pat, i)
return s:find(pat, i, plain)
end
function rfind(s, pat, i)
function rfind(s, pat, i, plain)
if s == nil then return end
local rs = s:reverse()
local rpat = pat:reverse()
if i == nil then i = #s end
local ri = #s - i + 1
local rendpos = rs:find(rpat, ri)
local rendpos = rs:find(rpat, ri, plain)
if rendpos == nil then return nil end
local endpos = #s - rendpos + 1
assert (endpos >= #pat)

View File

@ -118,7 +118,7 @@ function Text.compute_fragments(State, line_index)
for frag in line.data:gmatch('%S*%s*') do
local frag_text = App.newText(love.graphics.getFont(), frag)
local frag_width = App.width(frag_text)
--? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')
while x + frag_width > State.right do
--? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
if (x-State.left) < 0.8 * (State.right-State.left) then
@ -361,8 +361,7 @@ function Text.insert_return(State)
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
State.cursor1 = {line=State.cursor1.line+1, pos=1}
end
function Text.pageup(State)
@ -378,8 +377,7 @@ function Text.pageup(State)
top2 = Text.previous_screen_line(State, top2)
end
State.screen_top1 = Text.to1(State, top2)
State.cursor1.line = State.screen_top1.line
State.cursor1.pos = State.screen_top1.pos
State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
--? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
--? print('pageup end')
@ -398,12 +396,10 @@ function Text.pagedown(State)
if Text.lt1(State.screen_top1, new_top1) then
State.screen_top1 = new_top1
else
State.screen_top1.line = State.screen_bottom1.line
State.screen_top1.pos = State.screen_bottom1.pos
State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
end
--? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
State.cursor1.line = State.screen_top1.line
State.cursor1.pos = State.screen_top1.pos
State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
--? print('top now', State.screen_top1.line)
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
@ -468,6 +464,7 @@ function Text.down(State)
local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
--? print('cursor is NOT at final screen line of its line')
local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]
--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
@ -613,8 +610,12 @@ end
-- should never modify State.cursor1
function Text.snap_cursor_to_bottom_of_screen(State)
--? print('to2:', State.cursor1.line, State.cursor1.pos)
local top2 = Text.to2(State, State.cursor1)
--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
-- slide to start of screen line
top2.screen_pos = 1 -- start of screen line
--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
--? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
local y = App.screen.height - State.line_height
-- duplicate some logic from love.draw
@ -631,6 +632,7 @@ function Text.snap_cursor_to_bottom_of_screen(State)
--? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
State.screen_top1 = Text.to1(State, top2)
--? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
@ -787,7 +789,7 @@ function Text.x(s, pos)
end
function Text.to2(State, loc1)
local result = {line=loc1.line, screen_line=1}
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
Text.populate_screen_line_starting_pos(State, loc1.line)
for i=#line_cache.screen_line_starting_pos,1,-1 do