F52PSTYSIIUD7UPEVLEJPVSBQU7IKVHM4GN4YASNSKFP4MRE6S6AC
XZGCFXMOM4R7NCPCIYC4Z7Z3IFSKIFXFIF7AUULSTNZKEB7EUVBQC
QFANTTZDWNWYHNT3IMO4LX7DIYXOWZIUAEZRKETTJSTRRYX7PCLAC
DWRMU644UVLM5PSZDLZADZFKZ4VGZD3JTVOW7U3BY7WK42NYMUTAC
PD5L6SYFQHN6KMJ222UIGTFJJAJQDPV3TCKQK3TQFGQ6GDRT2F2AC
QMJNGKU3O3FCF4QUBAXNBOBVMSJV2DOFMNZK4CDSLR54WSIZ3OLAC
NAGDS5E2LF4PFXZS72KKDWQ5K5KMZXLV763Q4VCKIT6GOBAWB6MAC
SAATXLBLJS4VRZAFGDCKO45AKVFLS7TNXN4SZLBNOSPE7SKGKB2AC
2TQUKHBC2EB3WDBD5UL62DQYV7CV6B7OJYK7CHOEDNOZENSOG42AC
R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC
PMAQH5X6GKUHEDTVHJ4ZCMNG5OZUFZYQPYTCLMPZ7NFQCWSTGJFAC
A7O6MZIUNUHW65ODP7GLYO37RQ26AOUDGP5Z6M2RG77VZWKL27BQC
QNU7FBRQM62FXWTZO4KCZKBZLFYFSJZ4K5NN74V43FV2BLCEEB5QC
BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC
3VHUIIATPOF7FXB7NTL5MESCV5BCQACII2D7QZ4UIUCBX3CWXMMAC
F65ADDGLR2PNXVSM2XBHM3OSLQC2OTRR3GQBI7DJWIKPJCJ5CSOAC
LK4ZW4BBDD5LC4JK4XK5DJESSDFAIRVFPDM324S7SCAUXEXYVTLQC
KOTI3MFGQ4PDS4I75JIJG734LTET6745VGTSMNFYYASVIO6H2KPAC
3TI67SEJNOSADDEHRTI5FSRD7WVNRTXQ5LC77LBSWLXXZK3MCONQC
QCPXQ2E3USF3Z6R6WJ2JKHTRMPKA6QWXFKKRMLXA3MXABJEL543AC
KECEMMMRW2VVBZ567HJQPGLC57LTSBKWH7UFP32IW43D23X6WTEQC
2LEXWUW3EQUVE6IZTQROWRU6GDKZER35TGVDL3HFYYSEMA2RZEHAC
62JEPVQ34SOTQI6VQNLGLKS5O4KFU52UKAVDHN6N7G5T6Z5EZO5QC
2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC
4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC
LDFXFRUOUESGMZ7Z6BCZUQFRFFRIAB67GSGN2BR2VLT2ONZPUV3QC
5BMR5HRT7GN5L4XB4ISP4JJP3ONZESHEEQBCTQE4EVEDL7MBSDGAC
7VGDIPLCFDG3PVE4JH3WDKZ4A7PG5UYW7TLFFFOWN2JEUZYYTFJQC
U3MJNFUY4ER65BIKP4D7JGVYWFPXA55KBJYD4RH5KRRKRQXBAW5AC
O4RRXNOK7GKGZB2AH3FULDJDQLVSQCQFWZLBMCDGNIRA63OSSMCAC
JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC
GG2553RBUKMCWIAD3EWJV7C2QH2BIS5V2E3BDTFSTYRHAPXGBF7QC
LLQC2M2IMEJBJQXZTKC3OAKG5WKHSERXKAKCYHQRUZZD6CVRIHAQC
A4BSGS2CX4JK7IELL655EC6HAY6ILCWTGIHWZXHRGQOKU3HSUPLAC
3SMSYTKNRPZXV5TFOVAKW7FUPPPDSTG6BWVT3ZAXB23YCH36MXCAC
5E7BBR7HWGOEJJ6YWPL3NJKZVBTUZWET7OQNCRU2MMNVEGCEIAWQC
5XMBCKJZ7YCUOOQWWZRNLXMXD3MUYC2VAF3VKA4BIEHQRPDQMUMAC
3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC
D4B52CQ2QKG2HQKFUQOO5S2ME325DTW3PH2D7SBXCW4BPQFYG7CAC
B4FAIVRAXKCS2ZZXYUHL7HHWVMDMG2MQ3BY6IOJARK7B5XTBAQNAC
3IGFCRNEH3FIUWUFVRDDTGP3SRNPEJCWB3ELVEK6R45PRYXDKNSAC
VHUNJHXB4BQISKAXTMVB66C4QKF2NUZQRPRB37Z7OADC7PW5IMRAC
TOJA6VDEWPXDE4KE6URFLNIZBQ6ASAFDZMSYSIBDBKACBGUKZI3QC
VBU5YHLRO5ZSKFWBJRX7DWQGWPEHEWZMRRVV2WMWDJ54PKUNYCNQC
22K3VJIL4SRPXVAEJSWRS7KM4UPV4SOODPSTSSKPFWKLTYRG365QC
ORKN6EOBUFVAD2TXYW5OIKSL55RU24LOFDTTTXHDZUZ57QRDCY7QC
YJE7KP2XEC23N3V2XOP5CPORSHXXRQT2QVQQJALY3VKYRK74FSVAC
Y6PFNT7WA3C36JCPR4ZD4PKKB4JIIW46CDAKKP2C5EHGDVVCIONAC
SNQM42QARHDYXSD5NF3O5HFRL34SFEPYV4ZIC3WJL4KYSU7MD2YAC
U4CNVXMQKYY7ZXAPOSLR4GXDTIDEVOCDID5FUW5WMGLPC7FEHBRQC
S74FZY4PKFDUIG7G65MECQ4WJN67FDM755DCCQ2SSJFJX7H4UMWQC
SCOXD4EOOL76VOGQSSNYDQS4IPXJTQ7FABDLG66GLLNREYRZF5CQC
FXI74QCLOZ4BS7UVZ3U2PE3LOL7MX3FWGHZCTGH3DYFXGTXVVIRAC
EWQEFSZQN6JAJVENZMUKCUEB25GUE23U6DYSBNFNLB2NVUUDQ7ZQC
W2TDKP2P4SQ4MDJA74S6U4PF74XVD3ODG4KRYBHBHCB46ZKDUR5AC
564RK5FW46AHODXU7LMSGA5LQND5SIFQTZMBFEKYEA3KGMED4JBAC
OL33UKKSQH4TKDUZMAZFEU3AOOMO5NZWBTZZ6I7VIC73BQPZI26AC
KEFZWDCOCLPTLSZJKRV4VYAHRITV5T33YKG2VGT332YAUCOBS3EAC
UOTHQWM74AFOCPGQKKPLZXRI5HQFH3SRGI336AVZRGWPR6P256EAC
EMBBTRXDTL6DYNYUHOIZ2BXRCBS3BM6BPPZ4YLJAONG4K7DRSKNAC
FX6XTKP77BYLOGIJ7ZB7V5NGMMY7LRRG7X2C2426ANTF5NJNACIAC
LBOL46MAMM2KPDYLL2ZVYKP2RG5Z3M7CU7IACAAV47MOAU6MI45QC
FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC
GQ4CZKYS3N6JN55S7UWMQEZFADO26I3LV27IPGUJDIZT23Q5S3SAC
FY7BSGYTKQRCNTH26OD6SJMQPXNBBQNTN26RPXXWKNIQ7IKPXKMQC
-- Pong
pong = {}
function pong.initialize_globals()
N = 99
M = math.floor(N/2)
end
function pong.initialize(arg)
love.window.setMode(N*3, N*3) -- the court
if Settings and Settings.pong then
pong.load_settings()
else
if Settings == nil then Settings = {} end
if Settings.pong == nil then Settings.pong = {} end
Settings.pong.x, Settings.pong.y, Settings.pong.displayindex = love.window.getPosition()
end
pong.new_game()
end
function pong.new_game()
Ballp = {x=0, y=0}
Ballv = {x=love.math.random()-0.5, y=love.math.random()-0.5}
A = {ymin=-5, ymax=5} -- at x=-M
B = {ymin=-5, ymax=5} -- at x=+M
Pw, Pv = 3, 5 -- paddle width, velocity
Game_start = false
Game_over = falselog_new('game') log_start('trajectory')
end
function pong.draw()
love.graphics.scale(3, 3)
love.graphics.translate(M, M)
love.graphics.setColor(0,0,0)
love.graphics.circle('fill', Ballp.x, Ballp.y, 3)
love.graphics.rectangle('fill', -M, A.ymin, Pw, A.ymax-A.ymin)
love.graphics.rectangle('fill', M-Pw+1, B.ymin, Pw, B.ymax-B.ymin)
end
function pong.update(dt)
if not Game_start then return end
if Game_over then return end
Ballp.x = Ballp.x + Ballv.x
Ballp.y = Ballp.y + Ballv.y
if Ballp.y >= M or Ballp.y <= -M then
Ballv.y = -Ballv.ylog_state() log_new('trajectory')
end
if Ballp.x <= -M then
if Ballp.y >= A.ymin and Ballp.y <= A.ymax then
Ballv.x = -Ballv.xlog_state() log_new('trajectory')
else
Game_over = truelog_end('game')
end
end
if Ballp.x >= M then
if Ballp.y >= B.ymin and Ballp.y <= B.ymax then
Ballv.x = -Ballv.xlog_state() log_new('trajectory')
else
Game_over = truelog_end('game')
end
endlog_state()
end
Game_start = true
if chord == 'space' then
pong.new_game()
end
if Game_over then return end
if chord == 'down' then
end
if chord == 'up' then
end
if chord == 'a' then
end
if chord == 'z' then
end
end
Settings.pong.x, Settings.pong.y, Settings.pong.displayindex = love.window.getPosition()
end
return {
x=Settings.pong.x, y=Settings.pong.y, displayindex=Settings.pong.displayindex,
}
end
function pong.load_settings()
local settings = Settings.pong
end
-- vim:noexpandtab
--? print('loading position', settings.x, settings.y)
love.window.setPosition(settings.x, settings.y-37, settings.displayindex)
--? local x,y = love.window.getPosition()
--? print('after setposition', x,y)
function pong.settings()
if Current_app == 'pong' then
A.ymin, A.ymax = A.ymin+Pv, A.ymax+Pvlog(2, 'A down')
A.ymin, A.ymax = A.ymin-Pv, A.ymax-Pvlog(2, 'A up')
B.ymin, B.ymax = B.ymin-Pv, B.ymax-Pvlog(2, 'B up')
B.ymin, B.ymax = B.ymin+Pv, B.ymax+Pvlog(2, 'B down')
function pong.keychord_press(chord, key)
love.window.setTitle('pong.love')
love.graphics.setBackgroundColor(1,1,1)log_new('session')
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
--? print('snap', State.screen_top1.line, State.screen_top1.pos, 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
end
end
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
if #State.lines ~= #State.line_cache then
print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
assert(false)
end
if not Text.le1(State.screen_top1, State.cursor1) then
or not Text.le1(State.screen_top1, State.cursor1) then
State.screen_top1 = {line=1, pos=1}
end
end
function edit.invalid1(State, loc1)
return loc1.line > #State.lines
or loc1.pos > #State.lines[loc1.line].data
end
if State.font_height > 2 then
edit.update_font_settings(State, State.font_height-2)
Text.redraw_all(State)
end
elseif chord == 'C-0' then
-- environment for a mutable file
-- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.
Editor_state.cursor1 = {line=line.line_number, pos=1}
-- make sure it's visible
-- TODO: handle extremely long lines
Editor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)
-- show cursor
Focus = 'edit'
Log_browser_state.cursor1 = {line=1, pos=1}
end
Section_stack = {}
Section_border_color = {r=0.7, g=0.7, b=0.7}
Cursor_line_background_color = {r=0.7, g=0.7, b=0, a=0.1}
Section_border_padding_horizontal = 30 -- TODO: adjust this based on font height (because we draw text vertically along the borders
Section_border_padding_vertical = 15 -- TODO: adjust this based on font height
log_browser = {}
function log_browser.parse(State)
for _,line in ipairs(State.lines) do
if line.data ~= '' then
function test_up_arrow_scrolls_up_by_one_line_skipping_drawing()
-- display lines 3/4/5 with a drawing just off screen at line 2
App.screen.init{width=120, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', '```lines', '```', 'def', 'ghi', 'jkl'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=3, pos=1}
Editor_state.screen_top1 = {line=3, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'def', 'baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'baseline/screen:3')
-- after hitting the up arrow the screen scrolls up to previous text line
edit.run_after_keychord(Editor_state, 'up')
check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
check_eq(Editor_state.cursor1.line, 1, 'cursor')
end
end
function test_up_arrow_scrolls_up_by_one_line_skipping_drawing()
-- display lines 3/4/5 with a drawing just off screen at line 2
App.screen.init{width=120, height=60}
Editor_state = edit.initialize_test_state()
Editor_state.lines = load_array{'abc', '```lines', '```', 'def', 'ghi', 'jkl'}
Text.redraw_all(Editor_state)
Editor_state.cursor1 = {line=3, pos=1}
Editor_state.screen_top1 = {line=3, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'def', 'baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'baseline/screen:3')
-- after hitting the up arrow the screen scrolls up to previous text line
edit.run_after_keychord(Editor_state, 'up')
check_eq(Editor_state.screen_top1.line, 1, 'screen_top')
check_eq(Editor_state.cursor1.line, 1, 'cursor')
-- return the final y, and pos,posB of start of final screen line drawn
function Text.draw(State, line_index, y, startpos, startposB, hide_cursor)
-- return the final y, and position of start of final screen line drawn
function Text.draw(State, line_index, y, startpos, hide_cursor)
line_cache.startposB = startposB
-- draw A side
local overflows_screen, x, pos, screen_line_starting_pos
if startpos then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)
if overflows_screen then
return y, screen_line_starting_pos
end
if Focus == 'edit' and State.cursor1.pos then
if not hide_cursor and not State.search_term then
if line_index == State.cursor1.line and State.cursor1.pos == pos then
Text.draw_cursor(State, x, y)
end
end
end
else
x = State.left
end
-- check for B side
--? if line_index == 8 then print('checking for B side') end
if line.dataB == nil then
assert(y)
assert(screen_line_starting_pos)
--? if line_index == 8 then print('return 1') end
return y, screen_line_starting_pos
end
if not State.expanded and not line.expanded then
assert(y)
assert(screen_line_starting_pos)
--? if line_index == 8 then print('return 2') end
button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},
icon = function(button_params)
App.color(Fold_background_color)
love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)
end,
onpress1 = function()
line.expanded = true
end,
})
return y, screen_line_starting_pos
end
-- draw B side
--? if line_index == 8 then print('drawing B side') end
App.color(Fold_color)
if startposB then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)
else
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)
end
if overflows_screen then
return y, nil, screen_line_starting_pos
end
--? if line_index == 8 then print('a') end
if Focus == 'edit' and State.cursor1.posB then
--? if line_index == 8 then print('b') end
if not hide_cursor and not State.search_term then
--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
if line_index == State.cursor1.line and State.cursor1.posB == pos then
Text.draw_cursor(State, x, y)
end
end
end
return y, nil, screen_line_starting_pos
end
-- Given an array of fragments, draw the subset starting from pos to screen
-- starting from (x,y).
-- Return:
-- - whether we got to bottom of screen before end of line
-- - the final (x,y)
-- - the final pos
-- - starting pos of the final screen line drawn
function Text.draw_wrapping_line(State, line_index, x,y, startpos)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
--? print('== line', line_index, '^'..line.data..'$')
-- wrap long lines
local x = State.left
local pos = 1
return false, x,y, pos, screen_line_starting_pos
end
function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local screen_line_starting_pos = startpos
Text.compute_fragmentsB(State, line_index, x)
local pos = 1
for _, f in ipairs(line_cache.fragmentsB) do
local frag, frag_text = f.data, f.text
local frag_len = utf8.len(frag)
--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
if pos < startpos then
-- render nothing
--? print('skipping', frag)
else
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
assert(x > State.left) -- no overfull lines
y = y + State.line_height
if y + State.line_height > App.screen.height then
return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
end
screen_line_starting_pos = pos
x = State.left
end
if State.selection1.line then
local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
Text.draw_highlight(State, line, x,y, pos, lo,hi)
end
App.screen.draw(frag_text, x,y)
-- render cursor if necessary
if State.cursor1.posB and line_index == State.cursor1.line then
if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then
if State.search_term then
if State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term then
local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term))
App.color(Fold_color)
love.graphics.print(State.search_term, x+lo_px,y)
end
elseif Focus == 'edit' then
Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)
App.color(Fold_color)
end
end
end
x = x + frag_width
if Focus == 'edit' and not hide_cursor and State.search_term == nil then
if line_index == State.cursor1.line and State.cursor1.pos == pos then
Text.draw_cursor(State, x, y)
-- return the final y, and position of start of final screen line drawn
function Text.draw(State, line_index, y, startpos, hide_cursor)
-- wrap long lines
local x = State.left
local pos = 1
return y, screen_line_starting_pos
if line_index == State.cursor1.line then
if Focus == 'edit' and not hide_cursor and State.search_term == nil then
if line_index == State.cursor1.line and State.cursor1.pos == pos then
Text.draw_cursor(State, x, y)
return y, screen_line_starting_pos
end
x = x + frag_width
end
end
function Text.populate_screen_line_starting_posB(State, line_index, x)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
if line_cache.screen_line_starting_posB then
return
end
-- duplicate some logic from Text.draw
Text.compute_fragmentsB(State, line_index, x)
line_cache.screen_line_starting_posB = {1}
local pos = 1
for _, f in ipairs(line_cache.fragmentsB) do
local frag, frag_text = f.data, f.text
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
x = State.left
table.insert(line_cache.screen_line_starting_posB, pos)
end
x = x + frag_width
local frag_len = utf8.len(frag)
pos = pos + frag_len
end
end
function Text.compute_fragmentsB(State, line_index, x)
--? print('compute_fragmentsB', line_index, 'between', x, State.right)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
if line_cache.fragmentsB then
return
end
line_cache.fragmentsB = {}
-- try to wrap at word boundaries
for frag in line.dataB:gmatch('%S*%s*') do
local frag_text = App.newText(love.graphics.getFont(), frag)
local frag_width = App.width(frag_text)
--? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
while x + frag_width > State.right do
--? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
if (x-State.left) < 0.8 * (State.right-State.left) then
--? print('splitting')
-- long word; chop it at some letter
-- We're not going to reimplement TeX here.
local bpos = Text.nearest_pos_less_than(frag, State.right - x)
--? print('bpos', bpos)
if bpos == 0 then break end -- avoid infinite loop when window is too narrow
local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
local frag1 = string.sub(frag, 1, boffset-1)
local frag1_text = App.newText(love.graphics.getFont(), frag1)
local frag1_width = App.width(frag1_text)
--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
assert(x + frag1_width <= State.right)
table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})
frag = string.sub(frag, boffset)
frag_text = App.newText(love.graphics.getFont(), frag)
frag_width = App.width(frag_text)
end
x = State.left -- new line
end
if #frag > 0 then
--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})
if State.cursor1.pos then
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.pos = State.cursor1.pos+1
else
assert(State.cursor1.posB)
local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.posB = State.cursor1.posB+1
end
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.pos = State.cursor1.pos+1
end
elseif State.cursor1.posB then
if State.cursor1.posB > 1 then
before = snapshot(State, State.cursor1.line)
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
State.cursor1.posB = State.cursor1.posB-1
end
else
-- refuse to delete past beginning of side B
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
if State.cursor1.pos > 1 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)
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
end
elseif State.cursor1.posB then
if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
-- no change to State.cursor1.pos
end
else
-- refuse to delete past end of side B
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
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
Text.left(State)
if State.cursor1.pos then
-- when inserting a newline, move any B side to the new line
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
table.insert(State.line_cache, State.cursor1.line+1, {})
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
State.lines[State.cursor1.line].dataB = nil
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1 = {line=State.cursor1.line+1, pos=1}
else
-- disable enter when cursor is on the B side
end
end
function Text.pageup(State)
--? print('pageup')
-- duplicate some logic from love.draw
local top2 = Text.to2(State, State.screen_top1)
--? print(App.screen.height)
local y = App.screen.height - State.line_height
while y >= State.top do
--? print(y, top2.line, top2.screen_line, top2.screen_pos)
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
table.insert(State.line_cache, State.cursor1.line+1, {})
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1 = {line=State.cursor1.line+1, pos=1}
if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break end
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
table.insert(State.line_cache, State.cursor1.line+1, {})
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1 = {line=State.cursor1.line+1, pos=1}
if State.lines[State.screen_top1.line].mode == 'text' then
y = y - State.line_height
elseif State.lines[State.screen_top1.line].mode == 'drawing' then
y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
end
local bot2 = Text.to2(State, State.screen_bottom1)
local new_top1 = Text.to1(State, bot2)
if Text.lt1(State.screen_top1, new_top1) then
State.screen_top1 = new_top1
else
State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
-- If a line/paragraph gets to a page boundary, I often want to scroll
-- before I get to the bottom.
-- However, only do this if it makes forward progress.
if bot2.screen_line > 1 then
bot2.screen_line = math.max(bot2.screen_line-10, 1)
end
end
else
-- move up one screen line in current line
assert(screen_line_index > 1)
local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
--? print('cursor pos is now '..tostring(State.cursor1.pos))
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
function Text.upB(State)
local line_cache = State.line_cache[State.cursor1.line]
local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
assert(screen_line_indexB >= 1)
if screen_line_indexB == 1 then
-- move to A side of previous line
local new_cursor_line = State.cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if State.lines[new_cursor_line].mode == 'text' then
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local prev_line_cache = State.line_cache[State.cursor1.line]
local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
break
end
end
elseif screen_line_indexB == 2 then
-- all-B screen-line to potentially A+B screen-line
local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
if State.cursor_x < xA then
State.cursor1.posB = nil
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
else
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1
end
else
assert(screen_line_indexB > 2)
-- all-B screen-line to all-B screen-line
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
-- cursor on final screen line (A or B side) => goes to next screen line on A side
-- cursor on A side => move down one screen line (A side) in current line
-- cursor on B side => move down one screen line (B side) in current line
end
if State.cursor1.line > State.screen_bottom1.line then
--? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
--? print('scroll up preserving cursor')
Text.snap_cursor_to_bottom_of_screen(State)
--? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
end
end
else
-- move down one screen line (B side) in current line
local scroll_down = false
if Text.le1(State.screen_bottom1, State.cursor1) then
scroll_down = true
end
local cursor_line = State.lines[State.cursor1.line]
local cursor_line_cache = State.line_cache[State.cursor1.line]
local cursor2 = Text.to2(State, State.cursor1)
assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB)
local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = cursor_line_cache.screen_line_starting_posB[screen_line_indexB+1]
local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB)
local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
if scroll_down then
Text.snap_cursor_to_bottom_of_screen(State)
if State.cursor1.pos then
State.cursor1.pos = 1
else
State.cursor1.posB = 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
else
-- move down one screen line in current line
if State.cursor1.pos then
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
else
State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1
end
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_left(State)
Text.left(State)
Text.skip_non_whitespace_left(State)
end
function Text.word_right(State)
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_right(State)
Text.right(State)
Text.skip_non_whitespace_right(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.skip_whitespace_left(State)
if State.cursor1.pos then
Text.skip_whitespace_leftA(State)
else
Text.skip_whitespace_leftB(State)
end
end
function Text.skip_non_whitespace_left(State)
if State.cursor1.pos then
Text.skip_non_whitespace_leftA(State)
State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
-- skip some whitespace
-- skip some non-whitespace
function Text.word_right(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
assert(State.lines[State.cursor1.line].mode == 'text')
else
Text.skip_non_whitespace_leftB(State)
end
end
function Text.skip_whitespace_leftA(State)
if State.cursor1.posB == 1 then
break
end
assert(State.cursor1.posB > 1)
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then
break
end
Text.left(State)
end
end
function Text.skip_whitespace_right(State)
if State.cursor1.pos then
Text.skip_whitespace_rightA(State)
else
Text.skip_whitespace_rightB(State)
end
end
function Text.skip_non_whitespace_right(State)
if State.cursor1.pos then
Text.skip_non_whitespace_rightA(State)
else
Text.skip_non_whitespace_rightB(State)
end
end
function Text.skip_whitespace_rightA(State)
while true do
end
function Text.skip_non_whitespace_rightB(State)
while true do
if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then
break
end
Text.right_without_scroll(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
State.screen_top1 = {
line=State.cursor1.line,
pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
}
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
function Text.leftB(State)
if State.cursor1.posB > 1 then
State.cursor1.posB = State.cursor1.posB-1
else
-- overflow back into A side
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
else
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
end
end
end
function Text.right_without_scrollB(State)
if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
State.cursor1.posB = State.cursor1.posB+1
else
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
end
end
end
function Text.pos_at_start_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
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 spos,i
end
end
end
assert(false)
end
function Text.pos_at_start_of_screen_lineB(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
if sposB <= loc1.posB then
return sposB,i
if (not State.expanded and not line.expanded) or
line.dataB == nil then
return screen_lines[#screen_lines] <= State.cursor1.pos
end
if State.cursor1.pos then
-- ignore B side
return screen_lines[#screen_lines] <= State.cursor1.pos
end
assert(State.cursor1.posB)
local line_cache = State.line_cache[State.cursor1.line]
local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, State.cursor1.line, x)
local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB
return screen_lines[#screen_lines] <= State.cursor1.posB
return screen_lines[#screen_lines] <= State.cursor1.pos
if top2.screen_pos then
top2.screen_pos = 1
else
assert(top2.screen_posB)
top2.screen_posB = 1
end
--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
top2.screen_pos = 1 -- start of screen line
--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
--? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)
if top2.line == 1 and top2.screen_line == 1 then break end
--? print('to2:', State.cursor1.line, State.cursor1.pos)
--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
top2.screen_pos = 1 -- start of screen line
--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
local num_screen_lines = 0
if line_cache.startpos then
Text.populate_screen_line_starting_pos(State, line_index)
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1
end
--? print('#screenlines after A', num_screen_lines)
if line.dataB and (State.expanded or line.expanded) then
local x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, line_index, x)
--? print('B:', x, #line_cache.screen_line_starting_posB)
if line_cache.startposB then
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) -- no +1; first screen line of B side overlaps with A side
else
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1) -- no +1; first screen line of B side overlaps with A side
end
end
--? print('#screenlines after B', num_screen_lines)
return y < line_cache.starty + State.line_height*num_screen_lines
Text.populate_screen_line_starting_pos(State, line_index)
return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
-- returns: pos, posB
-- scenarios:
-- line without B side
-- line with B side collapsed
-- line with B side expanded
-- line starting rendering in A side (startpos ~= nil)
-- line starting rendering in B side (startposB ~= nil)
-- my on final screen line of A side
-- mx to right of A side with no B side
-- mx to right of A side but left of B side
-- mx to right of B side
-- preconditions:
-- startpos xor startposB
-- expanded -> dataB
--? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB)
if line_cache.startpos then
local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
local nexty = y + State.line_height
if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
--? print('past end of non-final line; return')
return line_cache.screen_line_starting_pos[screen_line_index+1]-1
end
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
local screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left)
if line.dataB == nil then
-- no B side
return screen_line_starting_pos + screen_line_posA - 1
end
if not State.expanded and not line.expanded then
-- B side is not expanded
return screen_line_starting_pos + screen_line_posA - 1
end
local lenA = utf8.len(s)
if screen_line_posA < lenA then
-- mx is within A side
return screen_line_starting_pos + screen_line_posA - 1
end
local max_xA = State.left+Text.x(s, lenA+1)
if mx < max_xA + AB_padding then
-- mx is in the space between A and B side
return screen_line_starting_pos + screen_line_posA - 1
end
mx = mx - max_xA - AB_padding
local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)
return nil, screen_line_posB
end
y = nexty
end
end
-- look in screen lines composed entirely of the B side
assert(State.expanded or line.expanded)
local start_screen_line_indexB
if line_cache.startposB then
start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)
else
start_screen_line_indexB = 2 -- skip the first line of side B, which we checked above
end
for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do
local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB]
local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB)
--? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
--? print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))
if screen_line_indexB < #line_cache.screen_line_starting_posB and mx > State.left + Text.screen_line_widthB(State, line_index, screen_line_indexB) then
if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
local s = string.sub(line.dataB, screen_line_starting_byte_offsetB)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)
return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1
end
local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
return App.width(screen_line_text)
end
function Text.screen_line_widthB(State, line_index, i)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local start_posB = line_cache.screen_line_starting_posB[i]
local start_offsetB = Text.offset(line.dataB, start_posB)
local screen_line
if i < #line_cache.screen_line_starting_posB then
--? print('non-final', i)
local past_end_posB = line_cache.screen_line_starting_posB[i+1]
local past_end_offsetB = Text.offset(line.dataB, past_end_posB)
--? print('between', start_offsetB, past_end_offsetB)
screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1)
else
--? print('final', i)
--? print('after', start_offsetB)
screen_line = string.sub(line.dataB, start_offsetB)
--? 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.populate_screen_line_starting_pos(State, line_index)
return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
return line_cache.screen_line_starting_pos[screen_line_index+1]-1
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1
if loc1.pos then
return Text.to2A(State, loc1)
else
return Text.to2B(State, loc1)
end
end
function Text.to2A(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
Text.populate_screen_line_starting_pos(State, loc1.line)
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
result.screen_line = i
result.screen_pos = loc1.pos - spos + 1
break
end
end
assert(result.screen_pos)
return result
end
function Text.to2B(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
Text.populate_screen_line_starting_pos(State, loc1.line)
local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
if sposB <= loc1.posB then
result.screen_lineB = i
result.screen_posB = loc1.posB - sposB + 1
break
end
end
assert(result.screen_posB)
function Text.to1B(State, loc2)
local result = {line=loc2.line, posB=loc2.screen_posB}
if loc2.screen_lineB > 1 then
result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1
end
return result
function Text.eq1(a, b)
return a.line == b.line and a.pos == b.pos
if State.lines[loc2.line-1].dataB == nil or
(not State.expanded and not State.lines[loc2.line-1].expanded) then
--? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
end
-- try to switch to B
local prev_line_cache = State.line_cache[loc2.line-1]
local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc2.line-1, x)
local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB
--? print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x)
if #screen_line_starting_posB > 1 then
--? print('c2')
return {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1}
else
--? print('c3')
-- if there's only one screen line, assume it overlaps with A, so remain in A
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
end
end
end
function Text.previous_screen_lineB(State, loc2)
if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A side
return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1}
else
-- switch to A side
-- TODO: handle case where fold lands precisely at end of a new screen-line
return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
local pos,posB = Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5)
State.cursor1 = {line=State.screen_bottom1.line, pos=pos, posB=posB}
State.cursor1 = {
line=State.screen_bottom1.line,
pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),
}
end
function Text.eq1(a, b)
return a.line == b.line and a.pos == b.pos
return a.pos < b.pos
function Text.le1(a, b)
if a.line < b.line then
if a.line > b.line then
return a.pos <= b.pos
elseif State.lines[loc2.line-1].mode == 'drawing' then
return {line=loc2.line-1, screen_line=1, screen_pos=1}
local l = State.lines[loc2.line-1]
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
State.cursor1 = {
line=State.screen_bottom1.line,
pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),
}
end
-- mode = 'text',
-- string data,
-- a drawing is a table with:
-- mode = 'drawing'
-- a (y) coord in pixels (updated while painting screen),
-- a (h)eight,
-- an array of points, and
-- an array of shapes
-- a shape is a table containing:
-- a mode
-- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing)
-- an array vertices for mode 'polygon', 'rectangle', 'square'
-- p1, p2 for mode 'line'
-- center, radius for mode 'circle'
-- center, radius, start_angle, end_angle for mode 'arc'
-- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide
-- The field names are carefully chosen so that switching modes in midstream
-- remembers previously entered points where that makes sense.
-- Lines can be too long to fit on screen, in which case they _wrap_ into
-- multiple _screen lines_.
-- rendering wrapped text lines needs some additional short-lived data per line:
-- startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen
-- starty, the y coord in pixels the line starts rendering from
-- fragments: snippets of rendered love.graphics.Text, guaranteed to not straddle screen lines
-- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line
line_cache = {},
-- Given wrapping, any potential location for the text cursor can be described in two ways:
-- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
-- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
screen_top1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at top of screen
cursor1 = {line=1, pos=1, posB=nil}, -- position of cursor
screen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen
screen_top1 = {line=1, pos=1}, -- position of start of screen line at top of screen
cursor1 = {line=1, pos=1}, -- position of cursor
screen_bottom1 = {line=1, pos=1}, -- position of start of screen line at bottom of screen
print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
--? print('draw:', y, line_index, line, line.mode)
--? print('draw:', y, line_index, line)
end
if line.data == '' then
-- button to insert new drawing
button(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,0},
icon = icon.insert_drawing,
onpress1 = function()
Drawing.before = snapshot(State, line_index-1, line_index)
table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
table.insert(State.line_cache, line_index, {})
if State.cursor1.line >= line_index then
State.cursor1.line = State.cursor1.line+1
end
schedule_save(State)
record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)})
end,
})
y = y + State.line_height
--? print('=> y', y)
elseif line.mode == 'drawing' then
y = y+Drawing_padding_top
Drawing.draw(State, line_index, y)
y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
else
print(line.mode)
assert(false)
State.cursor1.pos = State.cursor1.pos+1
cursor={line=State.cursor1.line, pos=State.cursor1.pos},
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
if State.cursor1.posB == nil then
local before = snapshot(State, State.cursor1.line)
if State.lines[State.cursor1.line].dataB == nil then
State.lines[State.cursor1.line].dataB = ''
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
return
end
for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then
-- delicate dance between cursor, selection and old cursor/selection
-- scenarios:
-- regular press+release: sets cursor, clears selection
-- shift press+release:
-- sets selection to old cursor if not set otherwise leaves it untouched
-- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift
-- i.e. mouse_release should never look at shift state
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y),
}
--? print('selection', State.selection1.line, State.selection1.pos)
break
State.lines[State.cursor1.line].expanded = true
State.cursor1.pos = nil
State.cursor1.posB = 1
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
elseif line.mode == 'drawing' then
local line_cache = State.line_cache[line_index]
if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
State.lines.current_drawing_index = line_index
State.lines.current_drawing = line
Drawing.before = snapshot(State, line_index)
Drawing.mouse_press(State, line_index, x,y, mouse_button)
break
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
end
end
end
function edit.mouse_release(State, x,y, mouse_button)
if State.search_term then return end
--? print('release')
if State.lines.current_drawing then
Drawing.mouse_release(State, x,y, mouse_button)
schedule_save(State)
if Drawing.before then
record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
Drawing.before = nil
end
else
for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then
--? print('reset selection')
State.cursor1 = {
line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y),
}
--? print('cursor', State.cursor1.line, State.cursor1.pos)
if State.mousepress_shift then
if State.old_selection1.line == nil then
State.selection1 = State.old_cursor1
else
State.selection1 = State.old_selection1
end
end
State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
if eq(State.cursor1, State.selection1) then
State.selection1 = {}
end
break
end
end
end
--? print('selection:', State.selection1.line, State.selection1.pos)
end
end
function edit.text_input(State, t)
if State.search_term then
State.search_term = State.search_term..t
State.search_text = nil
Text.search_next(State)
elseif State.current_drawing_mode == 'name' then
local before = snapshot(State, State.lines.current_drawing_index)
local drawing = State.lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
p.name = p.name..t
record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
else
local drawing_index, drawing = Drawing.current_drawing(State)
if drawing_index == nil then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
Text.text_input(State, t)
end
end
schedule_save(State)
end
function edit.keychord_press(State, chord, key)
if State.selection1.line and
not State.lines.current_drawing and
-- printable character created using shift key => delete selection
-- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
(not App.shift_down() or utf8.len(key) == 1) and
chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and not App.is_cursor_movement(chord) then
Text.delete_selection(State, State.left, State.right)
end
if State.search_term then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
if chord == 'escape' then
State.search_term = nil
State.search_text = nil
State.cursor1 = State.search_backup.cursor
State.screen_top1 = State.search_backup.screen_top
State.search_backup = nil
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
elseif chord == 'return' then
State.search_term = nil
State.search_text = nil
State.search_backup = nil
elseif chord == 'backspace' then
local len = utf8.len(State.search_term)
local byte_offset = Text.offset(State.search_term, len)
State.search_term = string.sub(State.search_term, 1, byte_offset-1)
State.search_text = nil
elseif chord == 'down' then
State.cursor1.pos = State.cursor1.pos+1
Text.search_next(State)
elseif chord == 'up' then
Text.search_previous(State)
function edit.eradicate_locations_after_the_fold(State)
-- eradicate side B from any locations we track
if State.cursor1.posB then
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
end
if State.screen_top1.posB then
State.screen_top1.posB = nil
State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
end
end
State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
if State.search_term then return end
--? print('press')
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
return
end
for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then
local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
--? print(x,y, 'setting cursor:', line_index, pos, posB)
-- delicate dance between cursor, selection and old cursor/selection
-- scenarios:
-- regular press+release: sets cursor, clears selection
-- shift press+release:
-- sets selection to old cursor if not set otherwise leaves it untouched
-- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift
-- i.e. mouse_release should never look at shift state
State.old_cursor1 = State.cursor1
State.old_selection1 = State.selection1
State.mousepress_shift = App.shift_down()
State.selection1 = {
line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y),
}
--? print('selection', State.selection1.line, State.selection1.pos)
break
end
elseif line.mode == 'drawing' then
local line_cache = State.line_cache[line_index]
if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
State.lines.current_drawing_index = line_index
State.lines.current_drawing = line
Drawing.before = snapshot(State, line_index)
Drawing.mouse_press(State, line_index, x,y, mouse_button)
break
end
end
end
end
function edit.mouse_release(State, x,y, mouse_button)
if State.search_term then return end
--? print('release')
if State.lines.current_drawing then
Drawing.mouse_release(State, x,y, mouse_button)
schedule_save(State)
if Drawing.before then
record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
Drawing.before = nil
end
else
for line_index,line in ipairs(State.lines) do
if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then
--? print('reset selection')
State.cursor1 = {
line=line_index,
pos=Text.to_pos_on_line(State, line_index, x, y),
}
--? print('cursor', State.cursor1.line, State.cursor1.pos)
if State.mousepress_shift then
if State.old_selection1.line == nil then
State.selection1 = State.old_cursor1
else
State.selection1 = State.old_selection1
end
end
State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
if eq(State.cursor1, State.selection1) then
State.selection1 = {}
end
break
end
end
end
--? print('selection:', State.selection1.line, State.selection1.pos)
end
end
function edit.text_input(State, t)
if State.search_term then
State.search_term = State.search_term..t
State.search_text = nil
Text.search_next(State)
elseif State.current_drawing_mode == 'name' then
local before = snapshot(State, State.lines.current_drawing_index)
local drawing = State.lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
p.name = p.name..t
record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
else
local drawing_index, drawing = Drawing.current_drawing(State)
if drawing_index == nil then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
Text.text_input(State, t)
end
end
schedule_save(State)
end
function edit.keychord_press(State, chord, key)
if State.selection1.line and
not State.lines.current_drawing and
-- printable character created using shift key => delete selection
-- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)
(not App.shift_down() or utf8.len(key) == 1) and
chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and not App.is_cursor_movement(chord) then
Text.delete_selection(State, State.left, State.right)
end
if State.search_term then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
if chord == 'escape' then
State.search_term = nil
State.search_text = nil
State.cursor1 = State.search_backup.cursor
State.screen_top1 = State.search_backup.screen_top
State.search_backup = nil
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
elseif chord == 'return' then
State.search_term = nil
State.search_text = nil
State.search_backup = nil
elseif chord == 'backspace' then
local len = utf8.len(State.search_term)
local byte_offset = Text.offset(State.search_term, len)
State.search_term = string.sub(State.search_term, 1, byte_offset-1)
State.search_text = nil
elseif chord == 'down' then
if State.cursor1.pos then
State.cursor1.pos = State.cursor1.pos+1
else
State.cursor1.posB = State.cursor1.posB+1
end
Text.search_next(State)
elseif chord == 'up' then
Text.search_previous(State)
end
return
elseif chord == 'C-f' then
State.search_term = ''
State.search_backup = {
cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},
}
assert(State.search_text == nil)
-- bifold text
elseif chord == 'M-b' then
State.expanded = not State.expanded
Text.redraw_all(State)
if not State.expanded then
for _,line in ipairs(State.lines) do
line.expanded = nil
end
edit.eradicate_locations_after_the_fold(State)
end
elseif chord == 'M-d' then
source.convert_bifold_text('pong.splua', 'pong.lua')
end
function source.convert_bifold_text(infilename, outfilename)
local contents = love.filesystem.read(infilename)
contents = contents:gsub('\u{1e}', ';')
love.filesystem.write(outfilename, contents)