integrated from pong.love: https://merveilles.town/@akkartik/108933336531898243
JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC UEE5W7WJ46FIBN4ZH45Z33L4RYXK5AP5ZIBHYTFOJTDWVVX54QKAC 7Q6GKOOL56VGLW3WCFTMXIGBIBNBJ5AD7LB5KJGZFAUUDIEYXE2QC TXDMRA5JEAML2GF5QY4ATU22G3NI7DQWPGO4U5OZNP7NGK4JT6WQC AGJXIDOFAZEKPSGDVZJAZXNOKQLTVZUKOCIYFN4AB4YEWVRFRNRAC 5F54FYKIMKS6TBV23M2GZ6N55VGHPBN66RO5XQS7ZY2GDZSSUJQAC Z6HI3K55DX3BK25SGJ5B5KYGNRPV6WH65QLN65BFPWD7L6WB7IHQC VNTRXQSX3XPYYZYP6ZJDIOCKQDMDBIZ2ZWRPSXPG37Q4IHCRDKIAC 2Y7YH7UPQWDNYDJN4BYY2MOHA36B2BIRX6DMIAKHJPQC7UP2R6NQC XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC 73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC H2DPLWMVRFYTO2CQTG54FMT2LF3B6UKLXH32CUA22DNQJVP5XBNQC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC 7PZ4CQFVYUMSJKVCNM75VKK5JCUYU6ICHWPZXXIC3S63YJVFCP5QC 5ZA3BRNYWKSGEBJ4JLA4UBC3LJPT5JBWYCU7PQYRSGX6MJMEWDIQC CE4LZV4TNXJT54CVGM3QANCBP42TMLMZWF2DBSMUYKAHILXIZEMQC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC 4CTZOJPCTWYUSHLIZZJ2M5W7S4JZFZVT5MUU5XNSOIBS5L4UY5UQC MD3W5IRAC6UQALQE4LJC52VQNDO3I3HXF3XE2XHDABXBYJBUVAXQC TVCPXAAU4P3K5MFYINH2MWDK3KGTQ2GE74TUNERYOONG2G5EYKMQC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC UH4YWHW5NDKNR7RS664UG4PRJNZIPNWAD5JWBEUB22JHOY2SWZKAC 6LJZN727CRPYR34LV75CQF55YZI3E7MGESYZSFSYAE73SNEZE3FAC BLWAYPKV3MLDZ4ALXLUJ25AIR6PCIL4RFYNRYLB26GFVC2KQBYBAC 2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC 6PUNJS5BSLTYMYMN4JFD7YDEGVQLM5PGAT7PQIG5NIAKLTM5T4PQC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC UHB4GARJI5AB5UCDCZRFSCJNXGJSLU5DYGUGX5ITYEXI7Q43Z4CAC 3QQZ7W4EJ7G4HQM5IYWXICMAHVRGERY4X6AOC6LOV5NSZ4OBICSAC LF7BWEG4DKQI7NMXMZC4LC2BE5PB42HK5PD6OYBNIDMAZBJASOKQC DJGC4ZEFQZAAKF3YWQKHMHAP2KANFVQ2H7KWAC4PU3HPHP6WLSOAC SDRXK4X5R6KBAFZTFWKTC7375HVVVPSTCDJVAYWSNUSHSKD242GQC K464QQR4FTXFUMHFWAGOD5DJ6YHUBUKRHLXF2ORE74DVT7TVQ35QC YGCT2D2ORMLTBHANLGHZV3EBGGHD7ZK55UAM7HF2AVSHDXAAKK5QC YKRF5V3ZZQIQ3UGAFYTQT5PUQVHCP2VHFDX77EY2C3X543HUDYKQC IZZVOCLB7KB4ZNQ35OL466MHWOK3XZMOS7ZPFLHUFQ47LJLQQQ3QC QCQHLMSTHTRNIKC5CJU3CAYMMWPTGITALRJNLFMGQWN2ZYMDSAWAC NEXUNNCF5PJC57XAMQGMSSYNI7MJ4ARWDY3HFGVYMGWG3MPHG7CQC HFI2YR2CWHWTAIQMDM6HIHHBUKQ74WA2QXW72PSKZWKHSVFWLKSQC QGO66DNKCM6NNXLSKKOVDLBIJ43UKN6APMF2BLO2KM4F3J7AH7KQC PGZJ6NATSMW4XH64XEPE5Q2EEYCMAMQIIP2OZXPNJ527234QPKMQC 46ASCE5K5QRO6BZNJPW4CJZCRVVG76S3GENIBGNGB352CP3DLDCQC PESSMQBJCOIA5PYNVKUG4D25VTFIG44QVCAOFRD4PKOJNW2AIHKAC RF5ALVNYB2FMU7LRRD5LMQC7P6OO4BX3NXIGWNZTQ2CD62RBRRFAC AVQ5MC5DWNLI6LUUIPGBLGP4LKRPGWBY4THNY25OBT2FAVHC6MCAC ESETRNLB3MIJ2SID6HJMMP52FEVUBLGK2HLWD75KDQZAKQMKSF2QC YTSPVDZHEN5LLNMGIBUBLPWFWSFM3SOHBRGWYSDEVFKRTH24ARRQC XSLCFVFHBXYPJDGOFULVB7UAWQY5CRDY4QKKHDXSZTSVLCHDL54QC W4UVZETRKOSWDPLAM5LGAPCQEJWIVFCXUJDVZQASEIKALYEU34KAC UYRAO73Y4LMTBBSH5RGNSNR532NFLRU5N5CJW3VIG72GZZXC654AC 2KRK3OBVPHQIDGCH2FBTP2AXKPEXS3OEPLBKU7UWCLKQA4MANGSAC SPNMXTYRSNPNQJNBTYDZSHYDZVZRPM4LI5QX7GR2TLTC6SPJX4DAC ILOA5BYFTQKBSHLFMMZUVPQ2JXBFJD62ERQFBTDK2WSRXUN525VQC ERQKFTPVWZO4WJD2WRIV33JWTWZSF4HNTK2GD7QT5I5TIL3SOGKQC R6GUSTBY5ZHR7E46DSIDQDNZDJI6QMZQDC7RPQMQWLGWQKXU6HVQC BJ5X5O4ACBBJ56LRBBSTCW6IBQP4HAEOOOPNH3SKTA4F66YTOIDAC YMH3YXXIVI7SIHH3PCWA7GQ374VVRLTQ74XTQEV22ZYUO5A4OGHQC HPVT467W763S6XQWS5Q47BAK4GMVY57LDXS7LSTFM23Y5XGKZMMQC WQOSZSUESLH4YRMW3PIWGSEC7RS243324PBROJP2KPRFJ3NFSEZQC MHOUX5JFGBFYMOULX3NZA2JXH6PF2227DT54EEXLBUZQFO7NDI2AC CNCYMM6ABOXCRI2IP5A4T2OGBO5FQ7GWBXBP2OQYL4YET5BLJCGQC 5MR22SGZE5YDU5CAIY53GNJDA6HSWBPYPD6M3FRQ5ZUMCSKTYJRAC AJB4LFRBMIRBEDWJ3OW7GQIMD2BZBVQ62GH4TE2FISWZKSAHRF4QC QCPXQ2E3USF3Z6R6WJ2JKHTRMPKA6QWXFKKRMLXA3MXABJEL543AC V7LATJC7BMSIZWVQKQXPS5ZYL24FDBMGPX54GV6FL2KNWIB5UTHQC PX7DDEMOBGPVK3FXKK5XEPG24CJXZSVW67DLG2JZZ5E77NVEAA3AC YT5P6TO64XSMCZGTT4SVNFOWUN5ECNXTWCMFXN3YCDZUNH4H3IFAC Z4KNS42NJZTQKUQZ7B5NYU2U4VOCUQCBFT2D7423MAXKF7NQ5ZJAC MGOQ5XAVFTWZPBG2O5ZTGSEKU6BRJKQZLDV6CM4737VD2FAEB5JQC 5T2E3PDVSLMZSSIIQRNKIKQVV77XQTHP473OP7XBTTMSZHIQID5AC LNUHQOGHIOFGJXNGA3DZLYEASLYYDGLN2I3EDZY5ANASQAHCG3YQC KVHUFUFVOSY6GB4XI2QK4T4WCLIYOV3NZR67TX6AQHAQDWJMEOBQC VIU2FBNVHG5FV5AJLVPMGEUO5HCLJEGZTRWNY2C5XC4AKMQZZKVAC 7M7LS7I2QT6AFZ6RVK5KK2CZ6SNJAMQIWD7MX34F7MQ3MZKH72GAC AM42E4Y6RLS7QPWBMESL6H5RPFKG5LQYM6EFNB5UYSRSUASKLISQC S7ZZA3YEKYGLBN6UC2N7WGUS43L6MX2KQQ2LBUZT4FQ7K7V5IQGQC DWZK32YDFQIUYR4LDUUFLOPJMUVJMFDP2RKW2L3QFAGUHAWPJU2AC EFMLTMZG5TUEGLSYLVKOKDSTGVSVWSKOMS7CJWOUGK5LADSH4YTQC O2UFJ6G3MDBJFSABWAJWTZGP6VAKRMQ6XCMILLQRRSS43C3UF2OQC ZUOL7X6VIPRCMEZURYGNHTDEIP3ZCHZW4PKVKBNXVZL5V4VOE5ZQC XX7G2FFJ4QCGQGD4REAW5QFHVYAKCFUPGZCK7L6DFGS5ISVBYBQQC NX3DDSCZM23ONUBXATHBM2DM3RL7YO7LDPXLI2UA6GQU2G3DKOTQC BYG5CEMVXANDTBI2ORNVMEY6K3EBRIHZHS4QBK27VONJC5537COQC 2ENZW7TVCS47BWCA4AIEVGKGMT4Y2TSM5IJ7O5K2VSWNXIN3SG4QC 6DE7RBZ6RHNEICJ7EUMCTROK43LW4LYINULIF2QEQOKCXWLUYUXAC JCSLDGAH2F6AIY4Z6XM6K4LOMW7EFY3E4NF5YXLMHLTYTX3A4Z3QC CIQN2MDEMWAASJAHOHMUZTI5PF4JV5SZSOBYYDCIIFYO2VHWULKAC HRWN5V6J6VMXS7WNSRGI7WMUSZ2OI52JJ4IK352VVSDZI4EF5HHQC 3HDWCPDIQ2R7N63GQIABVE5L63U42CLKNLVJQFKBOZH4BMJQA5XAC TGZAJUEFRK3NTCDMPIIG7U2TGLDHK4U3JDNFAYX7NHXTJYBYEZIAC 4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC E5FYDACSQNKJG4USM52I6C4KTN3U4Z47C4TK4QYC6RF2FFCZCYCAC FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC D4FEFHQCSILZFQ5VLWNXAIRZNUMCDNGJSM4UJ6T6FDMMIWYRYILQC 4AXV2HG4NMAHAWGWD35V5PXULNORCBEKY65UQT37XNYXCSB7TWEAC 66X36NZNEHWWURKSZ6G3XSC4CKQ7NTR4HYGDJGLHAUELMUAQC34AC 5XQ4Y7NU63X2WW4ZR4P46LX5GEOTE7JH3AUMTDQW5VZ53GELNP2QC VSBSWTE4IVQDRXLPQ7VTDIIEBEF7GMGRBHZ2IA73ZR6B2KZWI5JAC -- 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 operationsfunction record_undo_event(State, data)State.history[State.next_history] = dataState.next_history = State.next_history+1for i=State.next_history,#State.history doState.history[i] = nilendendfunction undo_event(State)if State.next_history > 1 then--? print('moving to history', State.next_history-1)State.next_history = State.next_history-1local result = State.history[State.next_history]return resultendendfunction 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+1return resultendend-- 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 thene = sendassert(#State.lines > 0)if s < 1 then s = 1 endif s > #State.lines then s = #State.lines endif e < 1 then e = 1 endif e > #State.lines then e = #State.lines end-- compare with App.initialize_globalslocal 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 fragmentsfor i=s,e dolocal line = State.lines[i]table.insert(event.lines, {data=line.data, dataB=line.dataB})endreturn eventendfunction 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--? endassert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(lines, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(lines, to.start_line+i-1, to.lines[i])endendfunction patch_placeholders(line_cache, from, to)assert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(line_cache, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(line_cache, to.start_line+i-1, {})endend-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080function deepcopy(obj, seen)if type(obj) ~= 'table' then return obj endif seen and seen[obj] then return seen[obj] endlocal s = seen or {}local result = setmetatable({}, getmetatable(obj))s[obj] = resultfor k,v in pairs(obj) doresult[deepcopy(k, s)] = deepcopy(v, s)endreturn resultendfunction minmax(a, b)return math.min(a,b), math.max(a,b)end
-- major tests for text editing flowsfunction test_initial_state()io.write('\ntest_initial_state')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)check_eq(#Editor_state.lines, 1, 'F - test_initial_state/#lines')check_eq(Editor_state.cursor1.line, 1, 'F - test_initial_state/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_initial_state/cursor:pos')check_eq(Editor_state.screen_top1.line, 1, 'F - test_initial_state/screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos')endfunction 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 itApp.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 upedit.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')endfunction 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.topApp.screen.check(y, 'a', 'F - test_insert_first_character/screen:1')endfunction test_press_ctrl()io.write('\ntest_press_ctrl')-- press ctrl while the cursor is on textApp.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')endfunction test_move_left()io.write('\ntest_move_left')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'a'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_left')endfunction test_move_right()io.write('\ntest_move_right')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'a'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_right')endfunction test_move_left_to_previous_line()io.write('\ntest_move_left_to_previous_line')App.screen.init{width=120, 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}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')check_eq(Editor_state.cursor1.line, 1, 'F - test_move_left_to_previous_line/line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_left_to_previous_line/pos') -- past end of lineendfunction test_move_right_to_next_line()io.write('\ntest_move_right_to_next_line')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- past end of lineedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.cursor1.line, 2, 'F - test_move_right_to_next_line/line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_right_to_next_line/pos')endfunction test_move_to_start_of_word()io.write('\ntest_move_to_start_of_word')App.screen.init{width=120, 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=3}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_to_start_of_word')endfunction test_move_to_start_of_previous_word()io.write('\ntest_move_to_start_of_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_to_start_of_previous_word')endfunction test_skip_to_previous_word()io.write('\ntest_skip_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=5} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_to_previous_word')endfunction test_skip_past_tab_to_previous_word()io.write('\ntest_skip_past_tab_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def\tghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=10} -- within third wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_past_tab_to_previous_word')endfunction test_skip_multiple_spaces_to_previous_word()io.write('\ntest_skip_multiple_spaces_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=6} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_multiple_spaces_to_previous_word')endfunction test_move_to_start_of_word_on_previous_line()io.write('\ntest_move_to_start_of_word_on_previous_line')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.line, 1, 'F - test_move_to_start_of_word_on_previous_line/line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_move_to_start_of_word_on_previous_line/pos')endfunction test_move_past_end_of_word()io.write('\ntest_move_past_end_of_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word')endfunction test_skip_to_next_word()io.write('\ntest_skip_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 8, 'F - test_skip_to_next_word')endfunction test_skip_past_tab_to_next_word()io.write('\ntest_skip_past_tab_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc\tdef'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'F - test_skip_past_tab_to_next_word')endfunction test_skip_multiple_spaces_to_next_word()io.write('\ntest_skip_multiple_spaces_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_multiple_spaces_to_next_word')endfunction test_move_past_end_of_word_on_next_line()io.write('\ntest_move_past_end_of_word_on_next_line')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.line, 2, 'F - test_move_past_end_of_word_on_next_line/line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word_on_next_line/pos')endfunction test_click_with_mouse()io.write('\ntest_click_with_mouse')-- display two lines with cursor on one of themApp.screen.init{width=50, 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.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')endfunction test_click_with_mouse_to_left_of_line()io.write('\ntest_click_with_mouse_to_left_of_line')-- display a line with the cursor in the middleApp.screen.init{width=50, 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=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click to the left of the lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)-- cursor moves to start of linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:pos')endfunction test_click_with_mouse_takes_margins_into_account()io.write('\ntest_click_with_mouse_takes_margins_into_account')-- display two lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_takes_margins_into_account/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_takes_margins_into_account/cursor:pos')endfunction test_click_with_mouse_on_empty_line()io.write('\ntest_click_with_mouse_on_empty_line')-- display two lines with the first one emptyApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the empty lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_empty_line/cursor')endfunction test_draw_text()io.write('\ntest_draw_text')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_draw_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_draw_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_draw_text/screen:3')endfunction test_draw_wrapping_text()io.write('\ntest_draw_wrapping_text')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_draw_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_draw_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fgh', 'F - test_draw_wrapping_text/screen:3')endfunction test_draw_word_wrapping_text()io.write('\ntest_draw_word_wrapping_text')App.screen.init{width=60, 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=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_draw_word_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'F - test_draw_word_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_draw_word_wrapping_text/screen:3')endfunction test_click_with_mouse_on_wrapping_line()io.write('\ntest_click_with_mouse_on_wrapping_line')-- display two lines with cursor on one of themApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line/cursor:pos')endfunction test_click_with_mouse_on_wrapping_line_takes_margins_into_account()io.write('\ntest_click_with_mouse_on_wrapping_line_takes_margins_into_account')-- display two lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:pos')endfunction test_draw_text_wrapping_within_word()-- arrange a screen line that needs to be split within a wordio.write('\ntest_draw_text_wrapping_within_word')App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abcd e fghijk', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abcd ', 'F - test_draw_text_wrapping_within_word/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'e fgh', 'F - test_draw_text_wrapping_within_word/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ijk', 'F - test_draw_text_wrapping_within_word/screen:3')endfunction test_draw_wrapping_text_containing_non_ascii()-- draw a long line containing non-ASCIIio.write('\ntest_draw_wrapping_text_containing_non_ascii')App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'madam I’m adam', 'xyz'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'mad', 'F - test_draw_wrapping_text_containing_non_ascii/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'am I', 'F - test_draw_wrapping_text_containing_non_ascii/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, '’m a', 'F - test_draw_wrapping_text_containing_non_ascii/screen:3')endfunction test_click_on_wrapping_line()io.write('\ntest_click_on_wrapping_line')-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_on_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line/baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line/cursor:pos')endfunction test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()io.write('\ntest_click_on_wrapping_line_rendered_from_partway_at_top_of_screen')-- display a wrapping line from its second screen lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}Editor_state.screen_top1 = {line=1, pos=7}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:pos')endfunction test_click_past_end_of_wrapping_line()io.write('\ntest_click_past_end_of_wrapping_line')-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_past_end_of_wrapping_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line/baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line/cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_wrapping_line_containing_non_ascii()io.write('\ntest_click_past_end_of_wrapping_line_containing_non_ascii')-- display a wrapping line containing non-ASCIIApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'I’m ad', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_word_wrapping_line()io.write('\ntest_click_past_end_of_word_wrapping_line')-- display a long line wrapping at a word boundary on a screen of more realistic lengthApp.screen.init{width=160, height=80}Editor_state = edit.initialize_test_state()-- 0 1 2-- 123456789012345678901Editor_state.lines = load_array{'the quick brown fox jumped over the lazy dog'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'the quick brown fox ', 'F - test_click_past_end_of_word_wrapping_line/baseline/screen:1')y = y + Editor_state.line_height-- click past the end of the screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')endfunction 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.topApp.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')endfunction test_insert_newline()io.write('\ntest_insert_newline')-- display a few linesApp.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.topApp.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')-- hitting the enter key splits the lineedit.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.topApp.screen.check(y, 'a', 'F - test_insert_newline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_newline/screen:3')endfunction test_insert_newline_at_start_of_line()io.write('\ntest_insert_newline_at_start_of_line')-- display a lineApp.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 lineedit.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')endfunction test_insert_from_clipboard()io.write('\ntest_insert_from_clipboard')-- display a few linesApp.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.topApp.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')-- paste some text including a newline, check that new line is createdApp.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.topApp.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3')endfunction test_move_cursor_using_mouse()io.write('\ntest_move_cursor_using_mouse')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cacheedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)check_eq(Editor_state.cursor1.line, 1, 'F - test_move_cursor_using_mouse/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_cursor_using_mouse/cursor:pos')endfunction test_pagedown()io.write('\ntest_pagedown')App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_pagedown/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pagedown/baseline/screen:2')-- after pagedown the bottom line becomes the topedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_pagedown/cursor')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_pagedown/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')endfunction test_pagedown_can_start_from_middle_of_long_wrapping_line()io.write('\ntest_pagedown_can_start_from_middle_of_long_wrapping_line')-- draw a few lines starting from a very long wrapping lineApp.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 pqr stu vwx yza bcd efg hij', 'XYZ'}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.topApp.screen.check(y, 'abc ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:3')-- after pagedown we scroll down the very long wrapping lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:pos')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')endfunction test_pagedown_never_moves_up()io.write('\ntest_pagedown_never_moves_up')-- draw the final screen line of a wrapping lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=9}Editor_state.screen_top1 = {line=1, pos=9}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- pagedown makes no changeedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_never_moves_up/screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_never_moves_up/screen_top:pos')endfunction test_down_arrow_moves_cursor()io.write('\ntest_down_arrow_moves_cursor')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=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first three lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/baseline/screen:3')-- after hitting the down arrow, the cursor moves down by 1 lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 1, 'F - test_down_arrow_moves_cursor/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_down_arrow_moves_cursor/cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/screen:3')endfunction test_down_arrow_scrolls_down_by_one_line()io.write('\ntest_down_arrow_scrolls_down_by_one_line')-- display the first three lines with the cursor on the bottom lineApp.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=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_line/screen_top')check_eq(Editor_state.cursor1.line, 4, 'F - test_down_arrow_scrolls_down_by_one_line/cursor')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_line/screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line()io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line')-- display the first three lines with the cursor on the bottom lineApp.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=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word()io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:3')endfunction test_page_down_followed_by_down_arrow_does_not_scroll_screen_up()io.write('\ntest_page_down_followed_by_down_arrow_does_not_scroll_screen_up')App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:3')-- after hitting pagedown the screen scrolls down to start of a long lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:pos')-- after hitting down arrow the screen doesn't scroll down further, and certainly doesn't scroll upedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:pos')y = Editor_state.topApp.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:3')endfunction test_up_arrow_moves_cursor()io.write('\ntest_up_arrow_moves_cursor')-- display the first 3 lines with the cursor on the bottom lineApp.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=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/baseline/screen:3')-- after hitting the up arrow the cursor moves up by 1 lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_moves_cursor/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_up_arrow_moves_cursor/cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/screen:3')endfunction test_up_arrow_scrolls_up_by_one_line()io.write('\ntest_up_arrow_scrolls_up_by_one_line')-- display the lines 2/3/4 with the cursor on line 2App.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.topApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_by_one_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/screen:3')endfunction test_up_arrow_scrolls_up_by_one_screen_line()io.write('\ntest_up_arrow_scrolls_up_by_one_screen_line')-- display lines starting from second screen line of a lineApp.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=6}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:2')-- after hitting the up arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:pos')endfunction test_up_arrow_scrolls_up_to_final_screen_line()io.write('\ntest_up_arrow_scrolls_up_to_final_screen_line')-- display lines starting just after a long lineApp.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.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up to final screen line of previous lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:3')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')check_eq(Editor_state.screen_top1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:pos')endfunction test_up_arrow_scrolls_up_to_empty_line()io.write('\ntest_up_arrow_scrolls_up_to_empty_line')-- display a screenful of text with an empty line just above it outside the screenApp.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.topApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/cursor')y = Editor_state.top-- empty first liney = y + Editor_state.line_heightApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:3')endfunction test_pageup()io.write('\ntest_pageup')App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}-- initially the last two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'F - test_pageup/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pageup/baseline/screen:2')-- after pageup the cursor goes to first lineedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup/cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_pageup/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup/screen:2')endfunction test_pageup_scrolls_up_by_screen_line()io.write('\ntest_pageup_scrolls_up_by_screen_line')-- display the first three lines with the cursor on the bottom lineApp.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.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_by_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup_scrolls_up_by_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/screen:3')endfunction test_pageup_scrolls_up_from_middle_screen_line()io.write('\ntest_pageup_scrolls_up_from_middle_screen_line')-- display a few lines starting from the middle of a line (Editor_state.cursor1.pos > 1)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=5}Editor_state.screen_top1 = {line=2, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3')endfunction test_enter_on_bottom_line_scrolls_down()io.write('\ntest_enter_on_bottom_line_scrolls_down')-- display a few lines with cursor on bottom lineApp.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.topApp.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')-- after hitting the enter key the screen scrolls downedit.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.topApp.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3')endfunction 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 screenApp.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.topApp.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 downedit.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.topApp.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')endfunction 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 screenApp.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 downedit.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.topApp.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')endfunction test_typing_on_bottom_line_scrolls_down()io.write('\ntest_typing_on_bottom_line_scrolls_down')-- display a few lines with cursor on bottom lineApp.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.topApp.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')y = y + Editor_state.line_heightApp.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 downedit.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.topApp.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')endfunction 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 lineApp.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.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:2')-- after hitting the left arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'left')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:pos')endfunction test_right_arrow_scrolls_down_in_wrapped_line()io.write('\ntest_right_arrow_scrolls_down_in_wrapped_line')-- display the first three lines with the cursor on the bottom lineApp.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.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the right arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.screen_top1.line, 2, 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 6, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:3')endfunction test_home_scrolls_up_in_wrapped_line()io.write('\ntest_home_scrolls_up_in_wrapped_line')-- display lines starting from second screen line of a lineApp.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.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:2')-- after hitting home the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'home')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_home_scrolls_up_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/cursor:pos')endfunction test_end_scrolls_down_in_wrapped_line()io.write('\ntest_end_scrolls_down_in_wrapped_line')-- display the first three lines with the cursor on the bottom lineApp.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.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting end the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'end')check_eq(Editor_state.screen_top1.line, 2, 'F - test_end_scrolls_down_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_end_scrolls_down_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 8, 'F - test_end_scrolls_down_in_wrapped_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')endfunction test_position_cursor_on_recently_edited_wrapping_line()-- draw a line wrapping over 2 screen linesio.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.topApp.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')y = y + Editor_state.line_heightApp.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 linesedit.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.topApp.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')y = y + Editor_state.line_heightApp.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 mouseedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)-- cursor should movecheck_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')endfunction test_backspace_can_scroll_up()io.write('\ntest_backspace_can_scroll_up')-- display the lines 2/3/4 with the cursor on line 2App.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.topApp.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')-- after hitting backspace the screen scrolls up by one lineedit.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.topApp.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3')endfunction 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 lineApp.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.topApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.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 lineedit.run_after_keychord(Editor_state, 'backspace')y = Editor_state.topApp.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')y = y + Editor_state.line_heightApp.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')endfunction test_backspace_past_line_boundary()io.write('\ntest_backspace_past_line_boundary')-- position cursor at start of a (non-first) lineApp.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 lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")endfunction 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 characteredit.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')local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')-- undoedit.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')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')endfunction 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 characteredit.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')local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')-- undo--? -- after undo, the backspaced key is selectededit.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')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')endfunction test_search()io.write('\ntest_search')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'd')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 2, 'F - test_search/1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/1/cursor:pos')-- reset cursorEditor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}-- search for second occurrenceedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'de')edit.run_after_keychord(Editor_state, 'down')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 4, 'F - test_search/2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/2/cursor:pos')endfunction test_search_upwards()io.write('\ntest_search_upwards')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}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)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')-- search for previous occurrenceedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.cursor1.line, 1, 'F - test_search_upwards/2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_upwards/2/cursor:pos')endfunction test_search_wrap()io.write('\ntest_search_wrap')App.screen.init{width=120, 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=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'return')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap/1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_wrap/1/cursor:pos')endfunction test_search_wrap_upwards()io.write('\ntest_search_wrap_upwards')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search upwards for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'up')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap_upwards/1/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_search_wrap_upwards/1/cursor:pos')end
-- text editor, particularly text drawing, horizontal wrap, vertical scrollingText = {}AB_padding = 20 -- space in pixels between A side and B side-- draw a line starting from startpos to screen at y between State.left and State.right-- return the final y, and pos,posB of start of final screen line drawnfunction Text.draw(State, line_index, y, startpos, startposB)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]line_cache.starty = yline_cache.startpos = startposline_cache.startposB = startposB-- draw A sidelocal overflows_screen, x, pos, screen_line_starting_posif startpos thenoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)if overflows_screen thenreturn y, screen_line_starting_posendif Focus == 'edit' and State.cursor1.pos thenif State.search_term == nil thenif line_index == State.cursor1.line and State.cursor1.pos == pos thenText.draw_cursor(State, x, y)endendendelsex = State.leftend-- check for B side--? if line_index == 8 then print('checking for B side') endif line.dataB == nil thenassert(y)assert(screen_line_starting_pos)--? if line_index == 8 then print('return 1') endreturn y, screen_line_starting_posendif not State.expanded and not line.expanded thenassert(y)assert(screen_line_starting_pos)--? if line_index == 8 then print('return 2') endbutton(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 = trueend,})return y, screen_line_starting_posend-- draw B side--? if line_index == 8 then print('drawing B side') endApp.color(Fold_color)--? if Foo then--? print('draw:', State.lines[line_index].data, "=====", State.lines[line_index].dataB, 'starting from x', x+AB_padding)--? endif startposB thenoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)elseoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)endif overflows_screen thenreturn y, nil, screen_line_starting_posend--? if line_index == 8 then print('a') endif Focus == 'edit' and State.cursor1.posB then--? if line_index == 8 then print('b') endif State.search_term == nil then--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) endif line_index == State.cursor1.line and State.cursor1.posB == pos thenText.draw_cursor(State, x, y)endendendreturn y, nil, screen_line_starting_posend-- 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 drawnfunction 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..'$')local screen_line_starting_pos = startposText.compute_fragments(State, line_index)local pos = 1initialize_color()for _, f in ipairs(line_cache.fragments) doApp.color(Text_color)local frag, frag_text = f.data, f.textselect_color(frag)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 fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenassert(x > State.left) -- no overfull linesy = y + State.line_heightif y + State.line_height > App.screen.height thenreturn --[[screen filled]] true, x,y, pos, screen_line_starting_posendscreen_line_starting_pos = posx = State.leftendApp.screen.draw(frag_text, x,y)-- render cursor if necessaryif State.cursor1.pos and line_index == State.cursor1.line thenif pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos thenif State.search_term thenif State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term thenlocal lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term))App.color(Text_color)love.graphics.print(State.search_term, x+lo_px,y)endelseif Focus == 'edit' thenText.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)App.color(Text_color)endendendx = x + frag_widthendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendfunction 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 = startposText.compute_fragmentsB(State, line_index, x)local pos = 1for _, f in ipairs(line_cache.fragmentsB) dolocal frag, frag_text = f.data, f.textlocal 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 fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenassert(x > State.left) -- no overfull linesy = y + State.line_heightif y + State.line_height > App.screen.height thenreturn --[[screen filled]] true, x,y, pos, screen_line_starting_posendscreen_line_starting_pos = posx = State.leftendApp.screen.draw(frag_text, x,y)-- render cursor if necessaryif State.cursor1.posB and line_index == State.cursor1.line thenif pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB thenif State.search_term thenif State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term thenlocal 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)endelseif Focus == 'edit' thenText.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)App.color(Fold_color)endendendx = x + frag_widthendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendfunction Text.draw_cursor(State, x, y)-- blink every 0.5sif math.floor(Cursor_time*2)%2 == 0 thenApp.color(Cursor_color)love.graphics.rectangle('fill', x,y, 3,State.line_height)endState.cursor_x = xState.cursor_y = y+State.line_heightendfunction Text.populate_screen_line_starting_pos(State, line_index)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.screen_line_starting_pos thenreturnend-- duplicate some logic from Text.drawText.compute_fragments(State, line_index)line_cache.screen_line_starting_pos = {1}local x = State.leftlocal pos = 1for _, f in ipairs(line_cache.fragments) dolocal frag, frag_text = f.data, f.text-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenx = State.lefttable.insert(line_cache.screen_line_starting_pos, pos)endx = x + frag_widthlocal frag_len = utf8.len(frag)pos = pos + frag_lenendendfunction Text.compute_fragments(State, line_index)--? print('compute_fragments', line_index, 'between', State.left, State.right)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.fragments thenreturnendline_cache.fragments = {}local x = State.left-- try to wrap at word boundariesfor frag in line.data:gmatch('%S*%s*') dolocal frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)--? 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--? 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 narrowlocal 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.fragments, {data=frag1, text=frag1_text})frag = string.sub(frag, boffset)frag_text = App.newText(love.graphics.getFont(), frag)frag_width = App.width(frag_text)endx = State.left -- new lineendif #frag > 0 then--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')table.insert(line_cache.fragments, {data=frag, text=frag_text})endx = x + frag_widthendendfunction 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 thenreturnend-- duplicate some logic from Text.drawText.compute_fragmentsB(State, line_index, x)line_cache.screen_line_starting_posB = {1}local pos = 1for _, f in ipairs(line_cache.fragmentsB) dolocal frag, frag_text = f.data, f.text-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenx = State.lefttable.insert(line_cache.screen_line_starting_posB, pos)endx = x + frag_widthlocal frag_len = utf8.len(frag)pos = pos + frag_lenendendfunction 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 thenreturnendline_cache.fragmentsB = {}-- try to wrap at word boundariesfor frag in line.dataB:gmatch('%S*%s*') dolocal 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 narrowlocal 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)endx = State.left -- new lineendif #frag > 0 then--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})endx = x + frag_widthendendfunction Text.textinput(State, t)if App.mouse_down(1) then return endif App.ctrl_down() or App.alt_down() or App.cmd_down() then return endlocal 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 thenText.populate_screen_line_starting_pos(State, State.cursor1.line)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endrecord_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endfunction Text.insert_at_cursor(State, t)if State.cursor1.pos thenlocal 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+1elseassert(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+1endend-- Don't handle any keys here that would trigger love.textinput above.function Text.keychord_pressed(State, chord)--? print('chord', chord)--== shortcuts that mutate textif chord == 'return' thenlocal before_line = State.cursor1.linelocal before = snapshot(State, before_line)Text.insert_return(State)if State.cursor_y > App.screen.height - State.line_height thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})elseif chord == 'tab' thenlocal 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 thenText.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)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})elseif chord == 'backspace' thenlocal beforeif State.cursor1.pos and State.cursor1.pos > 1 thenbefore = 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 thenif byte_end thenState.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)elseState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)endState.cursor1.pos = State.cursor1.pos-1endelseif State.cursor1.posB thenif State.cursor1.posB > 1 thenbefore = 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 thenif byte_end thenState.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)elseState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)endState.cursor1.posB = State.cursor1.posB-1endelse-- refuse to delete past beginning of side Bendelseif State.cursor1.line > 1 thenbefore = snapshot(State, State.cursor1.line-1, State.cursor1.line)-- join linesState.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].datatable.remove(State.lines, State.cursor1.line)table.remove(State.line_cache, State.cursor1.line)State.cursor1.line = State.cursor1.line-1endif State.screen_top1.line > #State.lines thenText.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) thenlocal 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 leaksendText.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' thenlocal beforeif State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenbefore = snapshot(State, State.cursor1.line)elsebefore = snapshot(State, State.cursor1.line, State.cursor1.line+1)endif State.cursor1.pos and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenlocal 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 thenif byte_end thenState.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)elseState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)end-- no change to State.cursor1.posendelseif State.cursor1.posB thenif State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) thenlocal 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 thenif byte_end thenState.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)elseState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)end-- no change to State.cursor1.posendelse-- refuse to delete past end of side Bendelseif State.cursor1.line < #State.lines then-- join linesState.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data-- delete side B on first lineState.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataBtable.remove(State.lines, State.cursor1.line+1)table.remove(State.line_cache, State.cursor1.line+1)endText.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 cursorelseif chord == 'left' thenText.left(State)elseif chord == 'right' thenText.right(State)elseif chord == 'S-left' thenText.left(State)elseif chord == 'S-right' thenText.right(State)-- C- hotkeys reserved for drawings, so we'll use M-elseif chord == 'M-left' thenText.word_left(State)elseif chord == 'M-right' thenText.word_right(State)elseif chord == 'M-S-left' thenText.word_left(State)elseif chord == 'M-S-right' thenText.word_right(State)elseif chord == 'home' thenText.start_of_line(State)elseif chord == 'end' thenText.end_of_line(State)elseif chord == 'S-home' thenText.start_of_line(State)elseif chord == 'S-end' thenText.end_of_line(State)elseif chord == 'up' thenText.up(State)elseif chord == 'down' thenText.down(State)elseif chord == 'S-up' thenText.up(State)elseif chord == 'S-down' thenText.down(State)elseif chord == 'pageup' thenText.pageup(State)elseif chord == 'pagedown' thenText.pagedown(State)elseif chord == 'S-pageup' thenText.pageup(State)elseif chord == 'S-pagedown' thenText.pagedown(State)endendfunction Text.insert_return(State)if State.cursor1.pos then-- when inserting a newline, move any B side to the new linelocal 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), 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 = nilText.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 sideendendfunction Text.pageup(State)--? print('pageup')-- duplicate some logic from love.drawlocal top2 = Text.to2(State, State.screen_top1)--? print(App.screen.height)local y = App.screen.height - State.line_heightwhile y >= State.top do--? print(y, top2.line, top2.screen_line, top2.screen_pos)if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break endy = y - State.line_heighttop2 = Text.previous_screen_line(State, top2)endState.screen_top1 = Text.to1(State, top2)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}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')endfunction Text.pagedown(State)--? print('pagedown')local bot2 = Text.to2(State, State.screen_bottom1)local new_top1 = Text.to1(State, bot2)if Text.lt1(State.screen_top1, new_top1) thenState.screen_top1 = new_top1elseState.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}end--? print('setting top to', State.screen_top1.line, State.screen_top1.pos)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}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--? print('pagedown end')endfunction Text.up(State)if State.cursor1.pos thenText.upA(State)elseText.upB(State)endendfunction Text.upA(State)--? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)if screen_line_starting_pos == 1 then--? print('cursor is at first screen line of its line')-- line is done; skip to previous text lineif State.cursor1.line > 1 then--? print('found previous text line')State.cursor1 = {line=State.cursor1.line-1, pos=nil}Text.populate_screen_line_starting_pos(State, State.cursor1.line)-- previous text line found, pick its final screen line--? print('has multiple screen lines')local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos--? print(#screen_line_starting_pos)screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1endelse-- move up one screen line in current lineassert(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))endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction 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 lineif State.cursor1.line > 1 thenState.cursor1.line = State.cursor1.line-1State.cursor1.posB = nilText.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) - 1endelseif screen_line_indexB == 2 then-- all-B screen-line to potentially A+B screen-linelocal xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_paddingif State.cursor_x < xA thenState.cursor1.posB = nilText.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) - 1elseText.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) - 1endelseassert(screen_line_indexB > 2)-- all-B screen-line to all-B screen-lineText.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) - 1endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endend-- 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 linefunction Text.down(State)--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)if Text.cursor_at_final_screen_line(State) then-- line is done, skip to next text line--? print('cursor at final screen line of its line')if State.cursor1.line < #State.lines thenState.cursor1 = {line = State.cursor1.line+1,pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line+1].data, State.cursor_x, State.left)}--? print(State.cursor1.pos)endif 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)endelseif State.cursor1.pos then-- move down one screen line (A side) in current linelocal 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)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', State.cursor1.line, State.cursor1.pos)if scroll_down then--? 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)endelse-- move down one screen line (B side) in current linelocal scroll_down = falseif Text.le1(State.screen_bottom1, State.cursor1) thenscroll_down = trueendlocal 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) - 1if scroll_down thenText.snap_cursor_to_bottom_of_screen(State)endend--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endfunction Text.start_of_line(State)if State.cursor1.pos thenState.cursor1.pos = 1elseState.cursor1.posB = 1endif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line, pos=1} -- copyendendfunction Text.end_of_line(State)if State.cursor1.pos thenState.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1elseState.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1endif Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.word_left(State)-- we can cross the fold, so check side A/B one level downText.skip_whitespace_left(State)Text.left(State)Text.skip_non_whitespace_left(State)endfunction Text.word_right(State)-- we can cross the fold, so check side A/B one level downText.skip_whitespace_right(State)Text.right(State)Text.skip_non_whitespace_right(State)if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.skip_whitespace_left(State)if State.cursor1.pos thenText.skip_whitespace_leftA(State)elseText.skip_whitespace_leftB(State)endendfunction Text.skip_non_whitespace_left(State)if State.cursor1.pos thenText.skip_non_whitespace_leftA(State)elseText.skip_non_whitespace_leftB(State)endendfunction Text.skip_whitespace_leftA(State)while true doif State.cursor1.pos == 1 thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') thenbreakendText.left(State)endendfunction Text.skip_whitespace_leftB(State)while true doif State.cursor1.posB == 1 thenbreakendif Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') thenbreakendText.left(State)endendfunction Text.skip_non_whitespace_leftA(State)while true doif State.cursor1.pos == 1 thenbreakendassert(State.cursor1.pos > 1)if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') thenbreakendText.left(State)endendfunction Text.skip_non_whitespace_leftB(State)while true doif State.cursor1.posB == 1 thenbreakendassert(State.cursor1.posB > 1)if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') thenbreakendText.left(State)endendfunction Text.skip_whitespace_right(State)if State.cursor1.pos thenText.skip_whitespace_rightA(State)elseText.skip_whitespace_rightB(State)endendfunction Text.skip_non_whitespace_right(State)if State.cursor1.pos thenText.skip_non_whitespace_rightA(State)elseText.skip_non_whitespace_rightB(State)endendfunction Text.skip_whitespace_rightA(State)while true doif State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') thenbreakendText.right_without_scroll(State)endendfunction Text.skip_whitespace_rightB(State)while true doif State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) thenbreakendif Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') thenbreakendText.right_without_scroll(State)endendfunction Text.skip_non_whitespace_rightA(State)while true doif State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') thenbreakendText.right_without_scroll(State)endendfunction Text.skip_non_whitespace_rightB(State)while true doif State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) thenbreakendif Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') thenbreakendText.right_without_scroll(State)endendfunction Text.match(s, pos, pat)local start_offset = Text.offset(s, pos)assert(start_offset)local end_offset = Text.offset(s, pos+1)assert(end_offset > start_offset)local curr = s:sub(start_offset, end_offset-1)return curr:match(pat)endfunction Text.left(State)if State.cursor1.pos thenText.leftA(State)elseText.leftB(State)endendfunction Text.leftA(State)if State.cursor1.pos > 1 thenState.cursor1.pos = State.cursor1.pos-1elseif State.cursor1.line > 1 thenState.cursor1 = {line = State.cursor1.line-1,pos = utf8.len(State.lines[State.cursor1.line-1].data) + 1,}endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction Text.leftB(State)if State.cursor1.posB > 1 thenState.cursor1.posB = State.cursor1.posB-1else-- overflow back into A sideState.cursor1.posB = nilState.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction Text.right(State)Text.right_without_scroll(State)if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.right_without_scroll(State)if State.cursor1.pos thenText.right_without_scrollA(State)elseText.right_without_scrollB(State)endendfunction Text.right_without_scrollA(State)if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenState.cursor1.pos = State.cursor1.pos+1elseif State.cursor1.line <= #State.lines-1 thenState.cursor1 = {line=State.cursor1.line+1, pos=1}endendfunction Text.right_without_scrollB(State)if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) thenState.cursor1.posB = State.cursor1.posB+1elseif State.cursor1.line <= #State.lines-1 then-- overflow back into A sideState.cursor1 = {line=State.cursor1.line+1, pos=1}endendfunction 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 dolocal spos = line_cache.screen_line_starting_pos[i]if spos <= loc1.pos thenreturn spos,iendendassert(false)endfunction 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_paddingText.populate_screen_line_starting_posB(State, loc1.line, x)for i=#line_cache.screen_line_starting_posB,1,-1 dolocal sposB = line_cache.screen_line_starting_posB[i]if sposB <= loc1.posB thenreturn sposB,iendendassert(false)endfunction Text.cursor_at_final_screen_line(State)Text.populate_screen_line_starting_pos(State, State.cursor1.line)local line = State.lines[State.cursor1.line]local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos--? print(screen_lines[#screen_lines], State.cursor1.pos)if (not State.expanded and not line.expanded) orline.dataB == nil thenreturn screen_lines[#screen_lines] <= State.cursor1.posendif State.cursor1.pos then-- ignore B sidereturn screen_lines[#screen_lines] <= State.cursor1.posendassert(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_paddingText.populate_screen_line_starting_posB(State, State.cursor1.line, x)local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posBreturn screen_lines[#screen_lines] <= State.cursor1.posBendfunction Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)if State.top > App.screen.height - State.line_height then--? print('scroll up')Text.snap_cursor_to_bottom_of_screen(State)endend-- should never modify State.cursor1function Text.snap_cursor_to_bottom_of_screen(State)--? print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)local top2 = Text.to2(State, State.cursor1)--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)-- slide to start of screen lineif top2.screen_pos thentop2.screen_pos = 1elseassert(top2.screen_posB)top2.screen_posB = 1end--? 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('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.drawwhile true do--? 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 endlocal h = State.line_heightif y - h < State.top thenbreakendy = y - htop2 = Text.previous_screen_line(State, top2)end--? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)State.screen_top1 = Text.to1(State, top2)--? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksFoo = trueendfunction Text.in_line(State, line_index, x,y)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.starty == nil then return false end -- outside current pageif y < line_cache.starty then return false endlocal num_screen_lines = 0if line_cache.startpos thenText.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) + 1end--? print('#screenlines after A', num_screen_lines)if line.dataB and (State.expanded or line.expanded) thenlocal x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_paddingText.populate_screen_line_starting_posB(State, line_index, x)--? print('B:', x, #line_cache.screen_line_starting_posB)if line_cache.startposB thennum_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 sideelsenum_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 sideendend--? print('#screenlines after B', num_screen_lines)return y < line_cache.starty + State.line_height*num_screen_linesend-- convert mx,my in pixels to schema-1 coordinates-- returns: pos, posB-- scenarios:-- line without B side-- line with B side collapsed-- line with B side expanded-- line starting rendering in A side (startpos ~= nil)-- line starting rendering in B side (startposB ~= nil)-- my on final screen line of A side-- mx to right of A side with no B side-- mx to right of A side but left of B side-- mx to right of B side-- preconditions:-- startpos xor startposB-- expanded -> dataBfunction Text.to_pos_on_line(State, line_index, mx, my)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]assert(my >= line_cache.starty)-- duplicate some logic from Text.drawlocal y = line_cache.starty--? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB)if line_cache.startpos thenlocal 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 dolocal 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_heightif 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]-1endlocal 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 sidereturn screen_line_starting_pos + screen_line_posA - 1endif not State.expanded and not line.expanded then-- B side is not expandedreturn screen_line_starting_pos + screen_line_posA - 1endlocal lenA = utf8.len(s)if screen_line_posA < lenA then-- mx is within A sidereturn screen_line_starting_pos + screen_line_posA - 1endlocal 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 sidereturn screen_line_starting_pos + screen_line_posA - 1endmx = mx - max_xA - AB_paddinglocal screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)return nil, screen_line_posBendy = nextyendend-- look in screen lines composed entirely of the B sideassert(State.expanded or line.expanded)local start_screen_line_indexBif line_cache.startposB thenstart_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)elsestart_screen_line_indexB = 2 -- skip the first line of side B, which we checked aboveendfor screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB dolocal 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 nexty = y + State.line_heightif my < nexty then-- On all wrapped screen lines but the final one, clicks past end of-- line position cursor on final character of screen line.-- (The final screen line positions past end of screen line as always.)--? print('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))if screen_line_indexB < #line_cache.screen_line_starting_posB and mx > State.left + Text.screen_line_widthB(State, line_index, screen_line_indexB) then--? print('past end of non-final line; return')return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1endlocal 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) - 1endy = nextyendassert(false)endfunction Text.screen_line_width(State, line_index, i)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]local start_pos = line_cache.screen_line_starting_pos[i]local start_offset = Text.offset(line.data, start_pos)local screen_lineif i < #line_cache.screen_line_starting_pos thenlocal past_end_pos = line_cache.screen_line_starting_pos[i+1]local past_end_offset = Text.offset(line.data, past_end_pos)screen_line = string.sub(line.data, start_offset, past_end_offset-1)elsescreen_line = string.sub(line.data, start_pos)endlocal screen_line_text = App.newText(love.graphics.getFont(), screen_line)return App.width(screen_line_text)endfunction 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_lineif 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)endlocal screen_line_text = App.newText(love.graphics.getFont(), screen_line)--? local result = App.width(screen_line_text)--? print('=>', result)--? return resultreturn App.width(screen_line_text)endfunction Text.screen_line_index(screen_line_starting_pos, pos)for i = #screen_line_starting_pos,1,-1 doif screen_line_starting_pos[i] <= pos thenreturn iendendendfunction Text.screen_line_indexB(screen_line_starting_posB, posB)if posB == nil thenreturn 0endassert(screen_line_starting_posB)for i = #screen_line_starting_posB,1,-1 doif screen_line_starting_posB[i] <= posB thenreturn iendendend-- convert x pixel coordinate to pos-- oblivious to wrapping-- result: 1 to len+1function Text.nearest_cursor_pos(line, x, left)if x < left thenreturn 1endlocal len = utf8.len(line)local max_x = left+Text.x(line, len+1)if x > max_x thenreturn len+1endlocal leftpos, rightpos = 1, len+1--? print('-- nearest', x)while true do--? print('nearest', x, '^'..line..'$', leftpos, rightpos)if leftpos == rightpos thenreturn leftposendlocal curr = math.floor((leftpos+rightpos)/2)local currxmin = left+Text.x(line, curr)local currxmax = left+Text.x(line, curr+1)--? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)if currxmin <= x and x < currxmax thenif x-currxmin < currxmax-x thenreturn currelsereturn curr+1endendif leftpos >= rightpos-1 thenreturn rightposendif currxmin > x thenrightpos = currelseleftpos = currendendassert(false)end-- return the nearest index of line (in utf8 code points) which lies entirely-- within x pixels of the left margin-- result: 0 to len+1function Text.nearest_pos_less_than(line, x)--? print('', '-- nearest_pos_less_than', line, x)local len = utf8.len(line)local max_x = Text.x_after(line, len)if x > max_x thenreturn len+1endlocal left, right = 0, len+1while true dolocal curr = math.floor((left+right)/2)local currxmin = Text.x_after(line, curr+1)local currxmax = Text.x_after(line, curr+2)--? print('', x, left, right, curr, currxmin, currxmax)if currxmin <= x and x < currxmax thenreturn currendif left >= right-1 thenreturn leftendif currxmin > x thenright = currelseleft = currendendassert(false)endfunction Text.x_after(s, pos)local offset = Text.offset(s, math.min(pos+1, #s+1))local s_before = s:sub(1, offset-1)--? print('^'..s_before..'$')local text_before = App.newText(love.graphics.getFont(), s_before)return App.width(text_before)endfunction Text.x(s, pos)local offset = Text.offset(s, pos)local s_before = s:sub(1, offset-1)local text_before = App.newText(love.graphics.getFont(), s_before)return App.width(text_before)endfunction Text.to2(State, loc1)if loc1.pos thenreturn Text.to2A(State, loc1)elsereturn Text.to2B(State, loc1)endendfunction 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 dolocal spos = line_cache.screen_line_starting_pos[i]if spos <= loc1.pos thenresult.screen_line = iresult.screen_pos = loc1.pos - spos + 1breakendendassert(result.screen_pos)return resultendfunction 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_paddingText.populate_screen_line_starting_posB(State, loc1.line, x)for i=#line_cache.screen_line_starting_posB,1,-1 dolocal sposB = line_cache.screen_line_starting_posB[i]if sposB <= loc1.posB thenresult.screen_lineB = iresult.screen_posB = loc1.posB - sposB + 1breakendendassert(result.screen_posB)return resultendfunction Text.to1(State, loc2)if loc2.screen_pos thenreturn Text.to1A(State, loc2)elsereturn Text.to1B(State, loc2)endendfunction Text.to1A(State, loc2)local result = {line=loc2.line, pos=loc2.screen_pos}if loc2.screen_line > 1 thenresult.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1endreturn resultendfunction Text.to1B(State, loc2)local result = {line=loc2.line, posB=loc2.screen_posB}if loc2.screen_lineB > 1 thenresult.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1endreturn resultendfunction Text.lt1(a, b)if a.line < b.line thenreturn trueendif a.line > b.line thenreturn falseend-- A side < B sideif a.pos and not b.pos thenreturn trueendif not a.pos and b.pos thenreturn falseendif a.pos thenreturn a.pos < b.poselsereturn a.posB < b.posBendendfunction Text.le1(a, b)return eq(a, b) or Text.lt1(a, b)endfunction Text.offset(s, pos1)if pos1 == 1 then return 1 endlocal result = utf8.offset(s, pos1)if result == nil thenprint(pos1, #s, s)endassert(result)return resultendfunction Text.previous_screen_line(State, loc2)if loc2.screen_pos thenreturn Text.previous_screen_lineA(State, loc2)elsereturn Text.previous_screen_lineB(State, loc2)endendfunction Text.previous_screen_lineA(State, loc2)if loc2.screen_line > 1 then--? print('a')return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}elseif loc2.line == 1 then--? print('b')return loc2elseText.populate_screen_line_starting_pos(State, loc2.line-1)if State.lines[loc2.line-1].dataB == nil or(not State.expanded and not State.lines[loc2.line-1].expanded) then--? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}end-- try to switch to Blocal 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_paddingText.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 Areturn {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}endendendfunction Text.previous_screen_lineB(State, loc2)if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A sidereturn {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-linereturn {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}endend-- resize helperfunction Text.tweak_screen_top_and_cursor(State)if State.screen_top1.pos == 1 then return endText.populate_screen_line_starting_pos(State, State.screen_top1.line)local line = State.lines[State.screen_top1.line]local line_cache = State.line_cache[State.screen_top1.line]for i=2,#line_cache.screen_line_starting_pos dolocal pos = line_cache.screen_line_starting_pos[i]if pos == State.screen_top1.pos thenbreakendif pos > State.screen_top1.pos then-- make sure screen top is at start of a screen linelocal prev = line_cache.screen_line_starting_pos[i-1]if State.screen_top1.pos - prev < pos - State.screen_top1.pos thenState.screen_top1.pos = prevelseState.screen_top1.pos = posendbreakendend-- make sure cursor is on screenif Text.lt1(State.cursor1, State.screen_top1) thenState.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}elseif State.cursor1.line >= State.screen_bottom1.line then--? print('too low')if Text.cursor_out_of_screen(State) then--? print('tweak')local pos,posB = Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5)State.cursor1 = {line=State.screen_bottom1.line, pos=pos, posB=posB}endendend-- slightly expensive since it redraws the screenfunction Text.cursor_out_of_screen(State)App.draw()return State.cursor_y == nil-- this approach is cheaper and almost works, except on the final screen-- where file ends above bottom of screen--? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)--? local botline1 = {line=State.cursor1.line, pos=botpos}--? return Text.lt1(State.screen_bottom1, botline1)endfunction Text.redraw_all(State)--? print('clearing fragments')State.line_cache = {}for i=1,#State.lines doState.line_cache[i] = {}endendfunction Text.clear_screen_line_cache(State, line_index)State.line_cache[line_index].fragments = nilState.line_cache[line_index].fragmentsB = nilState.line_cache[line_index].screen_line_starting_pos = nilState.line_cache[line_index].screen_line_starting_posB = nilendfunction trim(s)return s:gsub('^%s+', ''):gsub('%s+$', '')endfunction ltrim(s)return s:gsub('^%s+', '')endfunction rtrim(s)return s:gsub('%s+$', '')end
function test_resize_window()io.write('\ntest_resize_window')App.screen.init{width=300, height=300}Editor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Log_browser_state = edit.initialize_test_state()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')App.resize(200, 400)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 resizecheck_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')-- TODO: how to make assertions about when App.update got past the early exit?endfunction test_drop_file()io.write('\ntest_drop_file')App.screen.init{width=Editor_state.left+300, height=300}Editor_state = edit.initialize_test_state()App.filesystem['foo'] = 'abc\ndef\nghi\n'local fake_dropped_file = {opened = false,getFilename = function(self)return 'foo'end,open = function(self)self.opened = trueend,lines = function(self)assert(self.opened)return App.filesystem['foo']:gmatch('[^\n]+')end,close = function(self)self.opened = falseend,}App.filedropped(fake_dropped_file)check_eq(#Editor_state.lines, 3, 'F - test_drop_file/#lines')check_eq(Editor_state.lines[1].data, 'abc', 'F - test_drop_file/lines:1')check_eq(Editor_state.lines[2].data, 'def', 'F - test_drop_file/lines:2')check_eq(Editor_state.lines[3].data, 'ghi', 'F - test_drop_file/lines:3')edit.draw(Editor_state)endfunction 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 yetEditor_state.lines = load_array{'abc', 'def'}Editor_state.filename = 'foo'schedule_save(Editor_state)-- now drag a new file bar from the filesystemApp.filesystem['bar'] = 'abc\ndef\nghi\n'local fake_dropped_file = {opened = false,getFilename = function(self)return 'bar'end,open = function(self)self.opened = trueend,lines = function(self)assert(self.opened)return App.filesystem['bar']:gmatch('[^\n]+')end,close = function(self)self.opened = falseend,}App.filedropped(fake_dropped_file)-- filesystem now contains a file called foocheck_eq(App.filesystem['foo'], 'abc\ndef\n', 'F - test_drop_file_saves_previous')end
-- primitives for saving to file and loading from fileFold = '\x1e' -- ASCII RS (record separator)function file_exists(filename)local infile = App.open_for_reading(filename)if infile theninfile:close()return trueelsereturn falseendendfunction load_from_disk(State)local infile = App.open_for_reading(State.filename)State.lines = load_from_file(infile)if infile then infile:close() endendfunction load_from_file(infile)local result = {}if infile thenlocal infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)while true dolocal line = infile_next_line()if line == nil then break endlocal line_info = {}if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')elseline_info.data = lineendtable.insert(result, line_info)endendif #result == 0 thentable.insert(result, {data=''})endreturn resultendfunction save_to_disk(State)local outfile = App.open_for_writing(State.filename)if outfile == nil thenerror('failed to write to "'..State.filename..'"')endfor _,line in ipairs(State.lines) dooutfile:write(line.data)if line.dataB and #line.dataB > 0 thenoutfile:write(Fold)outfile:write(line.dataB)endoutfile:write('\n')endoutfile:close()endfunction file_exists(filename)local infile = App.open_for_reading(filename)if infile theninfile:close()return trueelsereturn falseendend-- for testsfunction load_array(a)local result = {}local next_line = ipairs(a)local i,line,drawing = 0, ''while true doi,line = next_line(a, i)if i == nil then break endlocal line_info = {}if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')elseline_info.data = lineendtable.insert(result, line_info)endif #result == 0 thentable.insert(result, {data=''})endreturn resultend
-- some constants people might like to tweakText_color = {r=0, g=0, b=0}Cursor_color = {r=1, g=0, b=0}Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering overHighlight_color = {r=0.7, g=0.7, b=0.9} -- selected textFold_color = {r=0, g=0.6, b=0}Fold_background_color = {r=0, g=0.7, b=0}Margin_top = 15Margin_left = 25Margin_right = 25edit = {}-- run in both tests and a real runfunction edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screenlocal result = {-- a line of bifold text consists of an A side and an optional B side, each of which is a string-- expanded: whether to show B sidelines = {{data='', dataB=nil, expanded=nil}}, -- array of lines-- 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 lineline_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.-- Positions (and screen line indexes) can be in either the A or the B side.---- Most of the time we'll only persist positions in schema 1, translating to-- schema 2 when that's convenient.---- Make sure these coordinates are never aliased, so that changing one causes-- action at a distance.screen_top1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at top of screencursor1 = {line=1, pos=1, posB=nil}, -- position of cursorscreen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen-- cursor coordinates in pixelscursor_x = 0,cursor_y = 0,font_height = font_height,line_height = line_height,em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character widthtop = top,left = left,right = right,width = right-left,filename = love.filesystem.getUserDirectory()..'/lines.txt',next_save = nil,-- undohistory = {},next_history = 1,-- searchsearch_term = nil,search_text = nil,search_backup = nil, -- stuff to restore when cancelling search}return resultend -- App.initialize_statefunction edit.draw(State)State.button_handlers = {}App.color(Text_color)assert(#State.lines == #State.line_cache)if not Text.le1(State.screen_top1, State.cursor1) thenprint(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)assert(false)endState.cursor_x = nilState.cursor_y = nillocal y = State.top--? print('== draw')for line_index = State.screen_top1.line,#State.lines dolocal line = State.lines[line_index]--? print('draw:', y, line_index, line)if y + State.line_height > App.screen.height then break endState.screen_bottom1 = {line=line_index, pos=nil, posB=nil}--? print('text.draw', y, line_index)local startpos, startposB = 1, nilif line_index == State.screen_top1.line thenif State.screen_top1.pos thenstartpos = State.screen_top1.poselsestartpos, startposB = nil, State.screen_top1.posBendendy, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)y = y + State.line_height--? print('=> y', y)endif State.search_term thenText.draw_search_bar(State)endendfunction edit.update(State, dt)if State.next_save and State.next_save < App.getTime() thensave_to_disk(State)State.next_save = nilendendfunction schedule_save(State)if State.next_save == nil thenState.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you didendendfunction edit.quit(State)-- make sure to save before quittingif State.next_save thensave_to_disk(State)endendfunction edit.mouse_pressed(State, x,y, mouse_button)if State.search_term then return end--? print('press', State.selection1.line, State.selection1.pos)if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then-- press on a button and it returned 'true' to short-circuitreturnendfor line_index,line in ipairs(State.lines) doif Text.in_line(State, line_index, x,y) thenlocal pos,posB = Text.to_pos_on_line(State, line_index, x, y)--? print(x,y, 'setting cursor:', line_index, pos, posB)State.cursor1 = {line=line_index, pos=pos, posB=posB}breakendendendfunction edit.mouse_released(State, x,y, mouse_button)endfunction edit.textinput(State, t)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollif State.search_term thenState.search_term = State.search_term..tState.search_text = nilText.search_next(State)elseText.textinput(State, t)endschedule_save(State)endfunction edit.keychord_pressed(State, chord, key)if State.search_term thenif chord == 'escape' thenState.search_term = nilState.search_text = nilState.cursor1 = State.search_backup.cursorState.screen_top1 = State.search_backup.screen_topState.search_backup = nilText.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenState.search_term = nilState.search_text = nilState.search_backup = nilelseif chord == 'backspace' thenlocal 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 = nilelseif chord == 'down' thenif State.cursor1.pos thenState.cursor1.pos = State.cursor1.pos+1elseState.cursor1.posB = State.cursor1.posB+1endText.search_next(State)elseif chord == 'up' thenText.search_previous(State)endreturnelseif chord == 'C-f' thenState.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 textelseif chord == 'C-b' thenState.expanded = not State.expandedText.redraw_all(State)if not State.expanded thenfor _,line in ipairs(State.lines) doline.expanded = nilendedit.eradicate_locations_after_the_fold(State)endelseif chord == 'C-d' thenif State.cursor1.posB == nil thenlocal before = snapshot(State, State.cursor1.line)if State.lines[State.cursor1.line].dataB == nil thenState.lines[State.cursor1.line].dataB = ''endState.lines[State.cursor1.line].expanded = trueState.cursor1.pos = nilState.cursor1.posB = 1if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})end-- zoomelseif chord == 'C-=' thenedit.update_font_settings(State, State.font_height+2)Text.redraw_all(State)elseif chord == 'C--' thenedit.update_font_settings(State, State.font_height-2)Text.redraw_all(State)elseif chord == 'C-0' thenedit.update_font_settings(State, 20)Text.redraw_all(State)-- undoelseif chord == 'C-z' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal event = undo_event(State)if event thenlocal src = event.beforeState.screen_top1 = deepcopy(src.screen_top)State.cursor1 = deepcopy(src.cursor)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 leaksText.redraw_all(State)schedule_save(State)endelseif chord == 'C-y' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal event = redo_event(State)if event thenlocal src = event.afterState.screen_top1 = deepcopy(src.screen_top)State.cursor1 = deepcopy(src.cursor)patch(State.lines, event.before, event.after)-- if we're scrolling, reclaim all fragments to avoid memory leaksText.redraw_all(State)schedule_save(State)end-- clipboardelseif chord == 'C-c' thenlocal s = Text.selection(State)if s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal s = Text.cut_selection(State, State.left, State.right)if s thenApp.setClipboardText(s)endschedule_save(State)elseif chord == 'C-v' thenfor _,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.linelocal before = snapshot(State, before_line)local clipboard_data = App.getClipboardText()for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenText.insert_return(State)elseText.insert_at_cursor(State, c)endendif Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})-- dispatch to textelsefor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollText.keychord_pressed(State, chord)endendfunction edit.eradicate_locations_after_the_fold(State)-- eradicate side B from any locations we trackif State.cursor1.posB thenState.cursor1.posB = nilState.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)endif State.screen_top1.posB thenState.screen_top1.posB = nilState.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)endendfunction edit.key_released(State, key, scancode)endfunction edit.update_font_settings(State, font_height)State.font_height = font_heightlove.graphics.setFont(love.graphics.newFont(Editor_state.font_height))State.line_height = math.floor(font_height*1.3)State.em = App.newText(love.graphics.getFont(), 'm')Text_cache = {}end--== some methods for testsTest_margin_left = 25function edit.initialize_test_state()-- if you change these values, tests will start failingreturn edit.initialize_state(15, -- top marginTest_margin_left,App.screen.width, -- right margin = 014, -- font height assuming default LÖVE font15) -- line heightend-- all textinput events are also keypresses-- TODO: handle chords of multiple keysfunction edit.run_after_textinput(State, t)edit.keychord_pressed(State, t)edit.textinput(State, t)edit.key_released(State, t)App.screen.contents = {}edit.draw(State)end-- not all keys are textinputfunction edit.run_after_keychord(State, chord)edit.keychord_pressed(State, chord)edit.key_released(State, chord)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_click(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_pressed(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_released(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_press(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_pressed(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_release(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_released(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)end
source = {}Editor_state = {}-- called both in tests and real runfunction source.initialize_globals()-- tests currently mostly clear their own stateShow_log_browser_side = falseFocus = 'edit'Show_file_navigator = falseFile_navigation = {candidates = {'run','run_tests','log','edit','text','search','select','undo','text_tests','file','source','source_tests','commands','log_browser','source_edit','source_text','source_undo','colorize','source_text_tests','source_file','main','button','keychord','app','test','json',},index = 1,}Menu_status_bar_height = nil -- initialized below-- a few text objects we can avoid recomputing unless the font changesText_cache = {}-- blinking cursorCursor_time = 0end-- called only for real runfunction source.initialize()love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)love.graphics.setBackgroundColor(1,1,1)if Settings and Settings.source thensource.load_settings()elsesource.initialize_default_settings()endsource.initialize_edit_side{'run.lua'}source.initialize_log_browser_side()Menu_status_bar_height = 5 + Editor_state.line_height + 5Editor_state.top = Editor_state.top + Menu_status_bar_heightLog_browser_state.top = Log_browser_state.top + Menu_status_bar_heightend-- environment for a mutable file of bifolded text-- TODO: some initialization is also happening in load_settings/initialize_default_settings. Clean that up.function source.initialize_edit_side(arg)if #arg > 0 thenEditor_state.filename = arg[1]load_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}elseload_from_disk(Editor_state)Text.redraw_all(Editor_state)endif #arg > 1 thenprint('ignoring commandline args after '..arg[1])end-- We currently start out with side B collapsed.-- Other options:-- * save all expanded state by line-- * expand all if any location is in side Bif Editor_state.cursor1.line > #Editor_state.lines thenEditor_state.cursor1 = {line=1, pos=1}endif Editor_state.screen_top1.line > #Editor_state.lines thenEditor_state.screen_top1 = {line=1, pos=1}endedit.eradicate_locations_after_the_fold(Editor_state)if rawget(_G, 'jit') thenjit.off()jit.flush()endendfunction source.load_settings()local settings = Settings.sourcelove.graphics.setFont(love.graphics.newFont(settings.font_height))-- maximize window to determine maximum allowable dimensionslove.window.setMode(0, 0) -- maximizeDisplay_width, Display_height, App.screen.flags = love.window.getMode()-- set up desired window dimensionsApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(Display_width, 200)App.screen.flags.minheight = math.min(Display_height, 200)App.screen.width, App.screen.height = settings.width, settings.height--? print('setting window from settings:', App.screen.width, App.screen.height)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)--? print('loading source position', settings.x, settings.y, settings.displayindex)source.set_window_position_from_settings(settings)Show_log_browser_side = settings.show_log_browser_sidelocal right = App.screen.width - Margin_rightif Show_log_browser_side thenright = App.screen.width/2 - Margin_rightendEditor_state = edit.initialize_state(Margin_top, Margin_left, right, settings.font_height, math.floor(settings.font_height*1.3))Editor_state.filename = settings.filenameEditor_state.screen_top1 = settings.screen_topEditor_state.cursor1 = settings.cursorendfunction source.set_window_position_from_settings(settings)-- setPosition doesn't quite seem to do what is asked of it on Linux.love.window.setPosition(settings.x, settings.y-37, settings.displayindex)endfunction source.initialize_default_settings()local font_height = 20love.graphics.setFont(love.graphics.newFont(font_height))local em = App.newText(love.graphics.getFont(), 'm')source.initialize_window_geometry(App.width(em))Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)Editor_state.font_height = font_heightEditor_state.line_height = math.floor(font_height*1.3)Editor_state.em = emendfunction source.initialize_window_geometry(em_width)-- maximize windowlove.window.setMode(0, 0) -- maximizeDisplay_width, Display_height, App.screen.flags = love.window.getMode()-- shrink height slightly to account for window decorationApp.screen.height = Display_height-100App.screen.width = 40*em_widthApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.width, 200)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)print('initializing source position')if Settings == nil then Settings = {} endif Settings.source == nil then Settings.source = {} endSettings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()endfunction source.resize(w, h)--? print(("Window resized to width: %d and height: %d."):format(w, h))App.screen.width, App.screen.height = w, hText.redraw_all(Editor_state)Editor_state.selection1 = {} -- no support for shift drag while we're resizingif Show_log_browser_side thenEditor_state.right = App.screen.width/2 - Margin_rightelseEditor_state.right = App.screen.width-Margin_rightendLog_browser_state.left = App.screen.width/2 + Margin_rightLog_browser_state.right = App.screen.width-Margin_rightEditor_state.width = Editor_state.right-Editor_state.leftText.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)--? print('end resize')endfunction source.filedropped(file)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)end-- clear the slate for the new fileEditor_state.filename = file:getFilename()file:open('r')Editor_state.lines = load_from_file(file)file:close()Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}end-- a copy of source.filedropped when given a filenamefunction source.switch_to_file(filename)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)end-- clear the slate for the new fileEditor_state.filename = filenameload_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}endfunction source.draw()source.draw_menu_bar()edit.draw(Editor_state)if Show_log_browser_side then-- dividerApp.color(Divider_color)love.graphics.rectangle('fill', App.screen.width/2-1,Menu_status_bar_height, 3,App.screen.height)--log_browser.draw(Log_browser_state)endendfunction source.update(dt)Cursor_time = Cursor_time + dtif App.mouse_x() < Editor_state.right thenedit.update(Editor_state, dt)elseif Show_log_browser_side thenlog_browser.update(Log_browser_state, dt)endendfunction source.quit()edit.quit(Editor_state)log_browser.quit(Log_browser_state)-- convert any bifold files hereendfunction source.convert_bifold_text(infilename, outfilename)local contents = love.filesystem.read(infilename)contents = contents:gsub('\u{1e}', ';')love.filesystem.write(outfilename, contents)endfunction source.settings()if Current_app == 'source' then--? print('reading source window position')Settings.source.x, Settings.source.y, Settings.source.displayindex = love.window.getPosition()endlocal filename = Editor_state.filenameif filename:sub(1,1) ~= '/' thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windowsend--? print('saving source settings', Settings.source.x, Settings.source.y, Settings.source.displayindex)return {x=Settings.source.x, y=Settings.source.y, displayindex=Settings.source.displayindex,width=App.screen.width, height=App.screen.height,font_height=Editor_state.font_height,filename=filename,screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1,show_log_browser_side=Show_log_browser_side,focus=Focus,}endfunction source.mouse_pressed(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it moves--? print('mouse click', x, y)--? print(Editor_state.left, Editor_state.right)--? print(Log_browser_state.left, Log_browser_state.right)if Editor_state.left <= x and x < Editor_state.right then--? print('click on edit side')if Focus ~= 'edit' thenFocus = 'edit'endedit.mouse_pressed(Editor_state, x,y, mouse_button)elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then--? print('click on log_browser side')if Focus ~= 'log_browser' thenFocus = 'log_browser'endlog_browser.mouse_pressed(Log_browser_state, x,y, mouse_button)for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scrollendendfunction source.mouse_released(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.mouse_released(Editor_state, x,y, mouse_button)elsereturn log_browser.mouse_released(Log_browser_state, x,y, mouse_button)endendfunction source.textinput(t)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.textinput(Editor_state, t)elsereturn log_browser.textinput(Log_browser_state, t)endendfunction source.keychord_pressed(chord, key)Cursor_time = 0 -- ensure cursor is visible immediately after it moves--? print('source keychord')if Show_file_navigator thenkeychord_pressed_on_file_navigator(chord, key)returnendif chord == 'C-l' then--? print('C-l')Show_log_browser_side = not Show_log_browser_sideif Show_log_browser_side thenApp.screen.width = Log_browser_state.right + Margin_rightelseApp.screen.width = Editor_state.right + Margin_rightend--? print('setting window:', App.screen.width, App.screen.height)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)--? print('done setting window')-- try to restore position if possible-- if the window gets wider the window manager may not respect thissource.set_window_position_from_settings(Settings.source)returnendif chord == 'C-g' thenShow_file_navigator = trueFile_navigation.index = 1returnendif Focus == 'edit' thenreturn edit.keychord_pressed(Editor_state, chord, key)elsereturn log_browser.keychord_pressed(Log_browser_state, chord, key)endendfunction source.key_released(key, scancode)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.key_released(Editor_state, key, scancode)elsereturn log_browser.keychord_pressed(Log_browser_state, chordkey, scancode)endend-- use this sparinglyfunction to_text(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endreturn Text_cache[s]end
run = {}Editor_state = {}-- called both in tests and real runfunction run.initialize_globals()-- tests currently mostly clear their own state-- a few text objects we can avoid recomputing unless the font changesText_cache = {}-- blinking cursorCursor_time = 0end-- called only for real runfunction run.initialize(arg)love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)love.graphics.setBackgroundColor(1,1,1)if Settings thenrun.load_settings()elserun.initialize_default_settings()endif #arg > 0 thenEditor_state.filename = arg[1]load_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}elseload_from_disk(Editor_state)Text.redraw_all(Editor_state)endlove.window.setTitle('text.love - '..Editor_state.filename)if #arg > 1 thenprint('ignoring commandline args after '..arg[1])endif rawget(_G, 'jit') thenjit.off()jit.flush()endendfunction run.load_settings()love.graphics.setFont(love.graphics.newFont(Settings.font_height))-- maximize window to determine maximum allowable dimensionsApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()-- set up desired window dimensionslove.window.setPosition(Settings.x, Settings.y, Settings.displayindex)App.screen.flags.resizable = trueApp.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.heightlove.window.setMode(App.screen.width, App.screen.height, App.screen.flags)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.filenameEditor_state.screen_top1 = Settings.screen_topEditor_state.cursor1 = Settings.cursorendfunction run.initialize_default_settings()local font_height = 20love.graphics.setFont(love.graphics.newFont(font_height))local em = App.newText(love.graphics.getFont(), 'm')run.initialize_window_geometry(App.width(em))Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)Editor_state.font_height = font_heightEditor_state.line_height = math.floor(font_height*1.3)Editor_state.em = emSettings = run.settings()endfunction run.initialize_window_geometry(em_width)-- maximize windowlove.window.setMode(0, 0) -- maximizeApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()-- shrink height slightly to account for window decorationApp.screen.height = App.screen.height-100App.screen.width = 40*em_widthApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.width, 200)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)endfunction run.resize(w, h)--? print(("Window resized to width: %d and height: %d."):format(w, h))App.screen.width, App.screen.height = w, hText.redraw_all(Editor_state)Editor_state.selection1 = {} -- no support for shift drag while we're resizingEditor_state.right = App.screen.width-Margin_rightEditor_state.width = Editor_state.right-Editor_state.leftText.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)endfunction run.filedropped(file)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)end-- clear the slate for the new fileApp.initialize_globals()Editor_state.filename = file:getFilename()file:open('r')Editor_state.lines = load_from_file(file)file:close()Text.redraw_all(Editor_state)love.window.setTitle('text.love - '..Editor_state.filename)endfunction run.draw()edit.draw(Editor_state)endfunction run.update(dt)Cursor_time = Cursor_time + dtedit.update(Editor_state, dt)endfunction run.quit()edit.quit(Editor_state)endfunction run.settings()local x,y,displayindex = love.window.getPosition()local filename = Editor_state.filenameif filename:sub(1,1) ~= '/' thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windowsendreturn {x=x, y=y, displayindex=displayindex,width=App.screen.width, height=App.screen.height,font_height=Editor_state.font_height,filename=filename,screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1}endfunction run.mouse_pressed(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_pressed(Editor_state, x,y, mouse_button)endfunction run.mouse_released(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_released(Editor_state, x,y, mouse_button)endfunction run.textinput(t)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.textinput(Editor_state, t)endfunction run.keychord_pressed(chord, key)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.keychord_pressed(Editor_state, chord, key)endfunction run.key_released(key, scancode)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.key_released(Editor_state, key, scancode)end-- use this sparinglyfunction to_text(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endreturn Text_cache[s]end
-- delegate most business logic to a layer that can be reused by other projectsrequire 'edit'Editor_state = {}-- called both in tests and real runfunction App.initialize_globals()-- tests currently mostly clear their own state-- a few text objects we can avoid recomputing unless the font changesText_cache = {}-- blinking cursorCursor_time = 0
load_file_from_source_or_save_directory('keychord.lua')load_file_from_source_or_save_directory('button.lua')
-- for hysteresis in a few placesLast_resize_time = App.getTime()Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700end
-- both sides require (different parts of) the logging frameworkload_file_from_source_or_save_directory('log.lua')
-- called only for real runfunction App.initialize(arg)love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)love.graphics.setBackgroundColor(1,1,1)
-- but some files we want to only load sometimesfunction App.load()
if #arg > 0 thenEditor_state.filename = arg[1]load_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}elseload_from_disk(Editor_state)Text.redraw_all(Editor_state)
if Current_app == nil thenCurrent_app = 'run'
if #arg > 1 thenprint('ignoring commandline args after '..arg[1])
if Current_app == 'run' thenload_file_from_source_or_save_directory('file.lua')load_file_from_source_or_save_directory('run.lua')load_file_from_source_or_save_directory('edit.lua')load_file_from_source_or_save_directory('text.lua')load_file_from_source_or_save_directory('search.lua')load_file_from_source_or_save_directory('select.lua')load_file_from_source_or_save_directory('undo.lua')load_file_from_source_or_save_directory('text_tests.lua')load_file_from_source_or_save_directory('run_tests.lua')elseload_file_from_source_or_save_directory('source_file.lua')load_file_from_source_or_save_directory('source.lua')load_file_from_source_or_save_directory('commands.lua')load_file_from_source_or_save_directory('source_edit.lua')load_file_from_source_or_save_directory('log_browser.lua')load_file_from_source_or_save_directory('source_text.lua')load_file_from_source_or_save_directory('search.lua')load_file_from_source_or_save_directory('select.lua')load_file_from_source_or_save_directory('source_undo.lua')load_file_from_source_or_save_directory('colorize.lua')load_file_from_source_or_save_directory('source_text_tests.lua')load_file_from_source_or_save_directory('source_tests.lua')
function load_settings()local settings = json.decode(love.filesystem.read('config'))love.graphics.setFont(love.graphics.newFont(settings.font_height))-- maximize window to determine maximum allowable dimensionsApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()-- set up desired window dimensionslove.window.setPosition(settings.x, settings.y, settings.displayindex)App.screen.flags.resizable = trueApp.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.heightlove.window.setMode(App.screen.width, App.screen.height, App.screen.flags)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.filenameEditor_state.screen_top1 = settings.screen_topEditor_state.cursor1 = settings.cursorend
function App.initialize_globals()if Current_app == 'run' thenrun.initialize_globals()elseif Current_app == 'source' thensource.initialize_globals()elseassert(false, 'unknown app "'..Current_app..'"')end
function initialize_default_settings()local font_height = 20love.graphics.setFont(love.graphics.newFont(font_height))local em = App.newText(love.graphics.getFont(), 'm')initialize_window_geometry(App.width(em))Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)Editor_state.font_height = font_heightEditor_state.line_height = math.floor(font_height*1.3)Editor_state.em = em
-- for hysteresis in a few placesLast_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700Last_resize_time = App.getTime()
function initialize_window_geometry(em_width)-- maximize windowlove.window.setMode(0, 0) -- maximizeApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()-- shrink height slightly to account for window decorationApp.screen.height = App.screen.height-100App.screen.width = 40*em_widthApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.width, 200)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
function App.initialize(arg)if Current_app == 'run' thenrun.initialize(arg)elseif Current_app == 'source' thensource.initialize(arg)elseassert(false, 'unknown app "'..Current_app..'"')endlove.window.setTitle('text.love - '..Current_app)
function App.resize(w, h)--? print(("Window resized to width: %d and height: %d."):format(w, h))App.screen.width, App.screen.height = w, hText.redraw_all(Editor_state)Editor_state.selection1 = {} -- no support for shift drag while we're resizingEditor_state.right = App.screen.width-Margin_rightEditor_state.width = Editor_state.right-Editor_state.leftText.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
function App.resize(w,h)if Current_app == 'run' thenif run.resize then run.resize(w,h) endelseif Current_app == 'source' thenif source.resize then source.resize(w,h) endelseassert(false, 'unknown app "'..Current_app..'"')end
-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)
if Current_app == 'run' thenif run.filedropped then run.filedropped(file) endelseif Current_app == 'source' thenif source.filedropped then source.filedropped(file) endelseassert(false, 'unknown app "'..Current_app..'"')endlove.window.setTitle('text.love - '..Current_app)endfunction App.focus(in_focus)if in_focus thenLast_focus_time = App.getTime()
-- clear the slate for the new fileApp.initialize_globals()Editor_state.filename = file:getFilename()file:open('r')Editor_state.lines = load_from_file(file)file:close()Text.redraw_all(Editor_state)love.window.setTitle('text.love - '..Editor_state.filename)
if Current_app == 'run' thenif run.focus then run.focus(in_focus) endelseif Current_app == 'source' thenif source.focus then source.focus(in_focus) endelseassert(false, 'unknown app "'..Current_app..'"')end
function love.quit()edit.quit(Editor_state)-- save some important settingslocal x,y,displayindex = love.window.getPosition()local filename = Editor_state.filenameif filename:sub(1,1) ~= '/' thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
function App.keychord_pressed(chord, key)-- ignore events for some time after window in focus (mostly alt-tab)if App.getTime() < Last_focus_time + 0.01 thenreturn
local settings = {x=x, y=y, displayindex=displayindex,width=App.screen.width, height=App.screen.height,font_height=Editor_state.font_height,filename=filename,screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1}love.filesystem.write('config', json.encode(settings))endfunction App.mousepressed(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_pressed(Editor_state, x,y, mouse_button)endfunction App.mousereleased(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_released(Editor_state, x,y, mouse_button)endfunction App.focus(in_focus)if in_focus thenLast_focus_time = App.getTime()
--if chord == 'C-e' then-- carefully save settingsif Current_app == 'run' thenlocal source_settings = Settings.sourceSettings = run.settings()Settings.source = source_settingsif run.quit then run.quit() endCurrent_app = 'source'elseif Current_app == 'source' thenSettings.source = source.settings()if source.quit then source.quit() endCurrent_app = 'run'elseassert(false, 'unknown app "'..Current_app..'"')endSettings.current_app = Current_applove.filesystem.write('config', json.encode(Settings))-- rebootload_file_from_source_or_save_directory('main.lua')App.undo_initialize()App.run_tests_and_initialize()returnendif Current_app == 'run' thenif run.keychord_pressed then run.keychord_pressed(chord, key) endelseif Current_app == 'source' thenif source.keychord_pressed then source.keychord_pressed(chord, key) endelseassert(false, 'unknown app "'..Current_app..'"')
Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.textinput(Editor_state, t)
--if Current_app == 'run' thenif run.textinput then run.textinput(t) endelseif Current_app == 'source' thenif source.textinput then source.textinput(t) endelseassert(false, 'unknown app "'..Current_app..'"')end
Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.keychord_pressed(Editor_state, chord, key)
--if Current_app == 'run' thenif run.key_released then run.key_released(chord, key) endelseif Current_app == 'source' thenif source.key_released then source.key_released(chord, key) endelseassert(false, 'unknown app "'..Current_app..'"')end
function App.keyreleased(key, scancode)-- ignore events for some time after window in focusif App.getTime() < Last_focus_time + 0.01 thenreturn
function App.mousepressed(x,y, mouse_button)--? print('mouse press', x,y)if Current_app == 'run' thenif run.mouse_pressed then run.mouse_pressed(x,y, mouse_button) endelseif Current_app == 'source' thenif source.mouse_pressed then source.mouse_pressed(x,y, mouse_button) endelseassert(false, 'unknown app "'..Current_app..'"')
-- use this sparinglyfunction to_text(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)
function App.mousereleased(x,y, mouse_button)if Current_app == 'run' thenif run.mouse_released then run.mouse_released(x,y, mouse_button) endelseif Current_app == 'source' thenif source.mouse_released then source.mouse_released(x,y, mouse_button) endelseassert(false, 'unknown app "'..Current_app..'"')
return Text_cache[s]
endfunction love.quit()if Current_app == 'run' thenlocal source_settings = Settings.sourceSettings = run.settings()Settings.source = source_settingselseSettings.source = source.settings()endSettings.current_app = Current_applove.filesystem.write('config', json.encode(Settings))if Current_app == 'run' thenif run.quit then run.quit() endelseif Current_app == 'source' thenif source.quit then source.quit() endelseassert(false, 'unknown app "'..Current_app..'"')end
-- environment for immutable logs-- optionally reads extensions for rendering some types from the source codebase that generated them---- We won't care too much about long, wrapped lines. If they lines get too-- long to manage, you need a better, graphical rendering for them. Load-- functions to render them into the log_render namespace.function source.initialize_log_browser_side()Log_browser_state = edit.initialize_state(Margin_top, Editor_state.right + Margin_right + Margin_left, (Editor_state.right+Margin_right)*2, Editor_state.font_height, Editor_state.line_height)Log_browser_state.filename = 'log'load_from_disk(Log_browser_state) -- TODO: pay no attention to Foldlog_browser.parse(Log_browser_state)Text.redraw_all(Log_browser_state)Log_browser_state.screen_top1 = {line=1, pos=1}Log_browser_state.cursor1 = {line=1, pos=nil}endSection_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 bordersSection_border_padding_vertical = 15 -- TODO: adjust this based on font heightlog_browser = {}function log_browser.parse(State)for _,line in ipairs(State.lines) doif line.data ~= '' thenline.filename, line.line_number, line.data = line.data:match('%[string "([^:]*)"%]:([^:]*):%s*(.*)')line.filename = guess_source(line.filename)line.line_number = tonumber(line.line_number)if line.data:sub(1,1) == '{' thenlocal data = json.decode(line.data)if log_render[data.name] thenline.data = dataendline.section_stack = table.shallowcopy(Section_stack)elseif line.data:match('\u{250c}') thenline.section_stack = table.shallowcopy(Section_stack) -- as it is at the beginninglocal section_name = line.data:match('\u{250c}%s*(.*)')table.insert(Section_stack, {name=section_name})line.section_begin = trueline.section_name = section_nameline.data = nilelseif line.data:match('\u{2518}') thenlocal section_name = line.data:match('\u{2518}%s*(.*)')if array.find(Section_stack, function(x) return x.name == section_name end) thenwhile table.remove(Section_stack).name ~= section_name do--endline.section_end = trueline.section_name = section_nameline.data = nilendline.section_stack = table.shallowcopy(Section_stack)else-- stringline.section_stack = table.shallowcopy(Section_stack)endelseline.section_stack = {}endendendfunction table.shallowcopy(x)return {unpack(x)}endfunction guess_source(filename)local possible_source = filename:gsub('%.lua$', '%.splua')if file_exists(possible_source) thenreturn possible_sourceelsereturn filenameendendfunction log_browser.draw(State)assert(#State.lines == #State.line_cache)local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())local y = State.topfor line_index = State.screen_top1.line,#State.lines doApp.color(Text_color)local line = State.lines[line_index]if y + State.line_height > App.screen.height then break endlocal height = State.line_heightif should_show(line) thenlocal xleft = render_stack_left_margin(State, line_index, line, y)local xright = render_stack_right_margin(State, line_index, line, y)if line.section_name thenApp.color(Section_border_color)local section_text = to_text(line.section_name)if line.section_begin thenlocal sectiony = y+Section_border_padding_verticallove.graphics.line(xleft,sectiony, xleft,y+State.line_height)love.graphics.line(xright,sectiony, xright,y+State.line_height)love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)love.graphics.draw(section_text, xleft+50,y)love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)else assert(line.section_end)local sectiony = y+State.line_height-Section_border_padding_verticallove.graphics.line(xleft,y, xleft,sectiony)love.graphics.line(xright,y, xright,sectiony)love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)love.graphics.draw(section_text, xleft+50,y)love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)endelseif type(line.data) == 'string' thenlocal old_left, old_right = State.left,State.rightState.left,State.right = xleft,xrighty = Text.draw(State, line_index, y, --[[startpos]] 1)State.left,State.right = old_left,old_rightelseheight = log_render[line.data.name](line.data, xleft, y, xright-xleft)endendif App.mouse_x() > Log_browser_state.left and line_index == mouse_line_index thenApp.color(Cursor_line_background_color)love.graphics.rectangle('fill', xleft,y, xright-xleft, height)endy = y + heightendendendfunction render_stack_left_margin(State, line_index, line, y)if line.section_stack == nil then-- assertion messagefor k,v in pairs(line) doprint(k)endendApp.color(Section_border_color)for i=1,#line.section_stack dolocal x = State.left + (i-1)*Section_border_padding_horizontallove.graphics.line(x,y, x,y+log_browser.height(State, line_index))if y < 30 thenlove.graphics.print(line.section_stack[i].name, x+State.font_height+5, y+5, --[[vertically]] math.pi/2)endif y > App.screen.height-log_browser.height(State, line_index) thenlove.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)endendreturn log_browser.left_margin(State, line)endfunction render_stack_right_margin(State, line_index, line, y)App.color(Section_border_color)for i=1,#line.section_stack dolocal x = State.right - (i-1)*Section_border_padding_horizontallove.graphics.line(x,y, x,y+log_browser.height(State, line_index))if y < 30 thenlove.graphics.print(line.section_stack[i].name, x, y+5, --[[vertically]] math.pi/2)endif y > App.screen.height-log_browser.height(State, line_index) thenlove.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)endendreturn log_browser.right_margin(State, line)endfunction should_show(line)-- Show a line if every single section it's in is expanded.for i=1,#line.section_stack dolocal section = line.section_stack[i]if not section.expanded thenreturn falseendendreturn trueendfunction log_browser.left_margin(State, line)return State.left + #line.section_stack*Section_border_padding_horizontalendfunction log_browser.right_margin(State, line)return State.right - #line.section_stack*Section_border_padding_horizontalendfunction log_browser.update(State, dt)endfunction log_browser.quit(State)endfunction log_browser.mouse_pressed(State, x,y, mouse_button)local line_index = log_browser.line_index(State, x,y)if line_index == nil then-- below lower marginreturnend-- leave some space to click without focusinglocal line = State.lines[line_index]local xleft = log_browser.left_margin(State, line)local xright = log_browser.right_margin(State, line)if x < xleft or x > xright thenreturnend-- if it's a section begin/end and the section is collapsed, expand it-- TODO: how to collapse?if line.section_begin or line.section_end then-- HACK: get section reference from next/previous linelocal new_sectionif line.section_begin thenif line_index < #State.lines thenlocal next_section_stack = State.lines[line_index+1].section_stackif next_section_stack thennew_section = next_section_stack[#next_section_stack]endendelseif line.section_end thenif line_index > 1 thenlocal previous_section_stack = State.lines[line_index-1].section_stackif previous_section_stack thennew_section = previous_section_stack[#previous_section_stack]endendendif new_section and new_section.expanded == nil thennew_section.expanded = truereturnendend-- open appropriate file in source sideif line.filename ~= Editor_state.filename thensource.switch_to_file(line.filename)end-- set cursorEditor_state.cursor1 = {line=line.line_number, pos=1, posB=nil}-- make sure it's visible-- TODO: handle extremely long linesEditor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)-- show cursorFocus = 'edit'-- expand B sideEditor_state.expanded = trueendfunction log_browser.line_index(State, mx,my)-- duplicate some logic from log_browser.drawlocal y = State.topfor line_index = State.screen_top1.line,#State.lines dolocal line = State.lines[line_index]if should_show(line) theny = y + log_browser.height(State, line_index)if my < y thenreturn line_indexendif y > App.screen.height then break endendendendfunction log_browser.mouse_released(State, x,y, mouse_button)endfunction log_browser.textinput(State, t)endfunction log_browser.keychord_pressed(State, chord, key)-- moveif chord == 'up' thenwhile State.screen_top1.line > 1 doState.screen_top1.line = State.screen_top1.line-1if should_show(State.lines[State.screen_top1.line]) thenbreakendendelseif chord == 'down' thenwhile State.screen_top1.line < #State.lines doState.screen_top1.line = State.screen_top1.line+1if should_show(State.lines[State.screen_top1.line]) thenbreakendendelseif chord == 'pageup' thenlocal y = 0while State.screen_top1.line > 1 and y < App.screen.height - 100 doState.screen_top1.line = State.screen_top1.line - 1if should_show(State.lines[State.screen_top1.line]) theny = y + log_browser.height(State, State.screen_top1.line)endendelseif chord == 'pagedown' thenlocal y = 0while State.screen_top1.line < #State.lines and y < App.screen.height - 100 doif should_show(State.lines[State.screen_top1.line]) theny = y + log_browser.height(State, State.screen_top1.line)endState.screen_top1.line = State.screen_top1.line + 1endendendfunction log_browser.height(State, line_index)local line = State.lines[line_index]if line.data == nil then-- section headerreturn State.line_heightelseif type(line.data) == 'string' thenreturn State.line_heightelseif line.height == nil then--? print('nil line height! rendering off screen to calculate')line.height = log_render[line.data.name](line.data, State.left, App.screen.height, State.right-State.left)endreturn line.heightendendfunction log_browser.keyreleased(State, key, scancode)end
function log(stack_frame_index, obj)local info = debug.getinfo(stack_frame_index, 'Sl')local msgif type(obj) == 'string' thenmsg = objelsemsg = json.encode(obj)endlove.filesystem.append('log', info.short_src..':'..info.currentline..': '..msg..'\n')end-- for section delimiters we'll use specific Unicode box charactersfunction log_start(name, stack_frame_index)if stack_frame_index == nil thenstack_frame_index = 3endlog(stack_frame_index, '\u{250c} ' .. name)endfunction log_end(name, stack_frame_index)if stack_frame_index == nil thenstack_frame_index = 3endlog(stack_frame_index, '\u{2518} ' .. name)endfunction log_new(name, stack_frame_index)if stack_frame_index == nil thenstack_frame_index = 4endlog_end(name, stack_frame_index)log_start(name, stack_frame_index)end-- vim:noexpandtab
Menu_background_color = {r=0.6, g=0.8, b=0.6}Menu_border_color = {r=0.6, g=0.7, b=0.6}Menu_command_color = {r=0.2, g=0.2, b=0.2}Menu_highlight_color = {r=0.5, g=0.7, b=0.3}function source.draw_menu_bar()if App.run_tests then return end -- disable in testsApp.color(Menu_background_color)love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_border_color)love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_command_color)Menu_cursor = 5if Show_file_navigator thensource.draw_file_navigator()returnendadd_hotkey_to_menu('ctrl+e: run')if Focus == 'edit' thenadd_hotkey_to_menu('ctrl+g: switch file')if Show_log_browser_side thenadd_hotkey_to_menu('ctrl+l: hide log browser')elseadd_hotkey_to_menu('ctrl+l: show log browser')endif Editor_state.expanded thenadd_hotkey_to_menu('ctrl+b: collapse debug prints')elseadd_hotkey_to_menu('ctrl+b: expand debug prints')endadd_hotkey_to_menu('ctrl+d: create/edit debug print')add_hotkey_to_menu('ctrl+f: find in file')add_hotkey_to_menu('alt+left alt+right: prev/next word')elseif Focus == 'log_browser' then-- nothing yetelseassert(false, 'unknown focus "'..Focus..'"')endadd_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')endfunction add_hotkey_to_menu(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()for i,file in ipairs(File_navigation.candidates) doif file == 'source' thenApp.color(Menu_border_color)love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)endadd_file_to_menu(file, i == File_navigation.index)endendfunction add_file_to_menu(s, cursor_highlight)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendif cursor_highlight thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)endApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction keychord_pressed_on_file_navigator(chord, key)if chord == 'escape' thenShow_file_navigator = falseelseif chord == 'return' thenlocal candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')source.switch_to_file(candidate)Show_file_navigator = falseelseif chord == 'left' thenif File_navigation.index > 1 thenFile_navigation.index = File_navigation.index-1endelseif chord == 'right' thenif File_navigation.index < #File_navigation.candidates thenFile_navigation.index = File_navigation.index+1endendend
-- State transitions while colorizing a single line.-- Just for comments and strings.-- Limitation: each fragment gets a uniform color so we can only change color-- at word boundaries.Next_state = {normal={{prefix='--', target='comment'},{prefix='"', target='dstring'},{prefix="'", target='sstring'},},dstring={{suffix='"', target='normal'},},sstring={{suffix="'", target='normal'},},-- comments are a sink}Comments_color = {r=0, g=0, b=1}String_color = {r=0, g=0.5, b=0.5}Divider_color = {r=0.7, g=0.7, b=0.7}Colors = {normal=Text_color,comment=Comments_color,sstring=String_color,dstring=String_color}Current_state = 'normal'function initialize_color()--? print('new line')Current_state = 'normal'endfunction select_color(frag)--? print('before', '^'..frag..'$', Current_state)switch_color_based_on_prefix(frag)--? print('using color', Current_state, Colors[Current_state])App.color(Colors[Current_state])switch_color_based_on_suffix(frag)--? print('state after suffix', Current_state)endfunction switch_color_based_on_prefix(frag)if Next_state[Current_state] == nil thenreturnendfrag = rtrim(frag)for _,edge in pairs(Next_state[Current_state]) doif edge.prefix and find(frag, edge.prefix, nil, --[[plain]] true) == 1 thenCurrent_state = edge.targetbreakendendendfunction switch_color_based_on_suffix(frag)if Next_state[Current_state] == nil thenreturnendfrag = rtrim(frag)for _,edge in pairs(Next_state[Current_state]) doif edge.suffix and rfind(frag, edge.suffix, nil, --[[plain]] true) == #frag thenCurrent_state = edge.targetbreakendendendfunction trim(s)return s:gsub('^%s+', ''):gsub('%s+$', '')endfunction ltrim(s)return s:gsub('^%s+', '')endfunction rtrim(s)return s:gsub('%s+$', '')end
-- save/restore various framework globals we care about -- only on very first loadfunction App.snapshot_love()if Love_snapshot then return endLove_snapshot = {}-- save the entire initial font; it doesn't seem reliably recreated using newFontLove_snapshot.initial_font = love.graphics.getFont()endfunction App.undo_initialize()love.graphics.setFont(Love_snapshot.initial_font)endfunction App.run_tests_and_initialize()App.load()App.run_tests()App.disable_tests()App.initialize_globals()App.initialize(love.arg.parseGameArguments(arg), arg)end
* Initializing settings:- from previous session- Filename as absolute path- Filename as relative path- from defaults
Initializing settings:- delete app settings, start; window opens running the text editor- quit while running the text editor, restart; window opens running the text editor in same position+dimensions- quit while editing source (color; no selection), restart; window opens editing source in same position+dimensions- start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions- start out editing source, move window, press ctrl+e twice; window is editing source in same position+dimensions- no log file; switching to source worksCode loading:* run love with directory; text editor runs* run love with zip file; text editor runs