KHPSHJN4BMTJ3CHUFQQZU7ZIQDOQDF3L5HV3TRT5OJMYICJAEB5QC TSK2OXU2FTB2X44SN73Z4W4O6IDV6G6V3QU5UVTAGQY52Z7AHECAC HKV72RZVJEOF5GCHCRKEBGC3FQN7AYETY7LKEJUXVIQAB4QPEPYQC RZSAOW6QLUFULJYQLEL4MUKG7477PA3QP5ZFJPOV25TCUWQ7R45AC QSKWIEU3LOYO4NFUNMMUHH4S5QCQ7FAIBZGXZUOTWWTRNVTMOOOAC B7YJ2CUAB44WIRBN63VKZFCPU47FKCYBNCOFCMBGA5XVG4MFHK6QC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC QEXZHD2VPCM4TAPP7PR2K2PIR4BVES5IZWC3T6ZRNJWKWOXFILNQC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC VYAIL5M4DLB7L3L34JX4XBBX6LZJKQDNC4TVNALP5L5XLUBM7TIQC UN7GKYV5YP5DQRKDYNYJTGX3CPXQYBVFJ7SLW44NWCN53VEZ3GAAC KMSL74GAMFNTAKGDKZFP2AMQXUMOC3XH373BO4IABZWBEP3YAXKAC C7CQOQ6ZDF3O66KAPNWO4QZWXGCYY6VKU3J7RUQKQTF46JTATRGAC 7P6XKA2DCFG5YJAVTCAZMQNNDJ6K6OO5AILBBCVYV37XKZNYKHPAC S2BZ3VEFKU4SYA2FYNYHKVSOXJBCQUZYI3DAGZAPGIHDLDLX44SAC 34QHL4KCLCWXMXY7CBWZIEMKL6BX62DACWANNPQPYUHSV2INSE2QC D2KRR2KTJJQCVGQYIEJDLV4NKMXBSNN6WOB62BVZ67KKMBRITD7AC ECUKZUSFVKW6Z4GOE3G4CEZRIOJR5XF5OWZSDNEHOJVYHTG24DLAC 5R2LPB3UGD6S6YTJKRFA7FOCTZERZW5ACCPP46KQWY7K3FZ7ASQAC 2VEIMUQXG6S776NS77WTIFFIPSA27XDTWJZZ7FW3MTS2OHLHT56AC A5ZROUM4HAX4DI6PKZF4XTS362MIJ52OOQRZVYJWGIVOH5XLZREAC SVA7IC5OQMN72OUQ7BLBM4SKAMTZOHYNKZTYOFMW6AY4577LUCWQC 4CCBB66RZIPCP5O6TWGXZE3MVZ7WE4FN5SJOHDMPENWSRS7NWZGAC GBSRQUT4QF5WCFVSTGSOU3BM6VCGPNBBG5WKDEGDGGCOUWTPEC2AC BW2IUB3KA4AKD35DYLCUCUM4Z32FMKGZNUBQBAEDIQJJYPA547MAC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC HOSPP2ANSW654DYRTC6CQUQA2GUKV6T2FI7QBKXD2DZS3R32IMGAC OI4FPFINEROK6GNDEMOBTGSPYIULCLRGGT5W3H7VLM7VFH22GMWQC UPCIYZEUIFO2UJ3WPAFOD7VLNZEIIYYGJQGEMJOP5TSSE5PM4ZWAC LSYLEVBDBZBGLSCXTRBW46WT4TUMMSPCH7M6HSNYI5SIH2WNPYEAC KOYAJWE4NJ2J4X3SHEAVMRXYZPZGOMTI7OX3PTUQIDIZ2GQI6UKAC QCPXQ2E3USF3Z6R6WJ2JKHTRMPKA6QWXFKKRMLXA3MXABJEL543AC MXA3RZYKUI4UF2ISY7JEF6VKX6NOPZMZH5SLLCZHRJKFIXXXDPSAC CVSRHMJ2BM4LPVG67ULIVQMP2NW3YY2JC2ZQBEA6EB5KVM4O2L5AC Z5HLXU4PJWWJJDBCK52NBD6PIRIA3TAN2BKZB5HBYFGIDBX4F5HAC GNQC72UXBU6KYXW6MXLNRGTLXV2VPQXMVCLYMJT6POTFXSF5ASJAC KOTI3MFGQ4PDS4I75JIJG734LTET6745VGTSMNFYYASVIO6H2KPAC EGH7XDBKE3R74VXLNTCAP5LJTRBPFUEMPS647MJARDGCMUHJG2QQC EAEGCJV5JOW46KCZKKPBFKZ4Z3SDB3X4R7TLNXFWCIQN5UCNSXFQC E4HEHLRTRRIZZV4UMGVPG3LU4KJIPO5WFCBXHK7TG6YDURIEBVJQC MSOQI3A5BC5PY2MZXZQAQ4EQDT4KICQJPN3YUZVDYTWXSPZWBLIAC LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC 7PZ4CQFVYUMSJKVCNM75VKK5JCUYU6ICHWPZXXIC3S63YJVFCP5QC 5ZA3BRNYWKSGEBJ4JLA4UBC3LJPT5JBWYCU7PQYRSGX6MJMEWDIQC KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC AMOPICKVRHMQERJLFPMAAEBV7TL5QACGGSBJWRCMV5R5O3KDVETAC CE4LZV4TNXJT54CVGM3QANCBP42TMLMZWF2DBSMUYKAHILXIZEMQC CNCYMM6ABOXCRI2IP5A4T2OGBO5FQ7GWBXBP2OQYL4YET5BLJCGQC 2WGHUWE6DWRNUD7AJNLGT4I6PZ2EIVDQCZ7V7V7WSYPBRPYUUDUQC V7LATJC7BMSIZWVQKQXPS5ZYL24FDBMGPX54GV6FL2KNWIB5UTHQC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC 4CTZOJPCTWYUSHLIZZJ2M5W7S4JZFZVT5MUU5XNSOIBS5L4UY5UQC TVCPXAAU4P3K5MFYINH2MWDK3KGTQ2GE74TUNERYOONG2G5EYKMQC UH4YWHW5NDKNR7RS664UG4PRJNZIPNWAD5JWBEUB22JHOY2SWZKAC 2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC SDRXK4X5R6KBAFZTFWKTC7375HVVVPSTCDJVAYWSNUSHSKD242GQC K464QQR4FTXFUMHFWAGOD5DJ6YHUBUKRHLXF2ORE74DVT7TVQ35QC QZ2SXLHF6G3RBSLFIOYM3AQYWLGPWRTASNNQMOQBI5ASUAYCO6UAC YGCT2D2ORMLTBHANLGHZV3EBGGHD7ZK55UAM7HF2AVSHDXAAKK5QC UHB4GARJI5AB5UCDCZRFSCJNXGJSLU5DYGUGX5ITYEXI7Q43Z4CAC R6GUSTBY5ZHR7E46DSIDQDNZDJI6QMZQDC7RPQMQWLGWQKXU6HVQC BJ5X5O4ACBBJ56LRBBSTCW6IBQP4HAEOOOPNH3SKTA4F66YTOIDAC ERQKFTPVWZO4WJD2WRIV33JWTWZSF4HNTK2GD7QT5I5TIL3SOGKQC 5STHSG4UB2SC4EZWOQHPQM43BLC4X2EJTNSSYRF35XEYVMTOID5AC WQOSZSUESLH4YRMW3PIWGSEC7RS243324PBROJP2KPRFJ3NFSEZQC VNJ2M36434Q4S67OFJWFMCUPFYNNJGT4TFUQTIXAMMJLWHULHGSAC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC 5MR22SGZE5YDU5CAIY53GNJDA6HSWBPYPD6M3FRQ5ZUMCSKTYJRAC NEXUNNCF5PJC57XAMQGMSSYNI7MJ4ARWDY3HFGVYMGWG3MPHG7CQC 46ASCE5K5QRO6BZNJPW4CJZCRVVG76S3GENIBGNGB352CP3DLDCQC AJB4LFRBMIRBEDWJ3OW7GQIMD2BZBVQ62GH4TE2FISWZKSAHRF4QC YT5P6TO64XSMCZGTT4SVNFOWUN5ECNXTWCMFXN3YCDZUNH4H3IFAC 3QQZ7W4EJ7G4HQM5IYWXICMAHVRGERY4X6AOC6LOV5NSZ4OBICSAC S7ZZA3YEKYGLBN6UC2N7WGUS43L6MX2KQQ2LBUZT4FQ7K7V5IQGQC TXDMRA5JEAML2GF5QY4ATU22G3NI7DQWPGO4U5OZNP7NGK4JT6WQC JCSLDGAH2F6AIY4Z6XM6K4LOMW7EFY3E4NF5YXLMHLTYTX3A4Z3QC HRWN5V6J6VMXS7WNSRGI7WMUSZ2OI52JJ4IK352VVSDZI4EF5HHQC LNUHQOGHIOFGJXNGA3DZLYEASLYYDGLN2I3EDZY5ANASQAHCG3YQC JRLBUB6LR2JIAKVQNKF3T4BDICUIJ3HEMRRHX56YP5M5SP7ZS3WAC VG75U7IM2ZQTGM2QETDT6QQ4CSLQPB4APK436POAAQJWOMINPIJAC 6LJZN727CRPYR34LV75CQF55YZI3E7MGESYZSFSYAE73SNEZE3FAC 2Y7YH7UPQWDNYDJN4BYY2MOHA36B2BIRX6DMIAKHJPQC7UP2R6NQC 3HDWCPDIQ2R7N63GQIABVE5L63U42CLKNLVJQFKBOZH4BMJQA5XAC SPNMXTYRSNPNQJNBTYDZSHYDZVZRPM4LI5QX7GR2TLTC6SPJX4DAC NFI42KGXEHTNACH2CATBLJIAXJYNV2C3QBPR2ORPJZKOVEREVM7QC A4STVUZI2VJL2XRTERNBUQBOFWZJK5VHMNYFWYZJD7KETR6UZAOQC ZJOSQFN6CH475MQZ5EXWSWILKNC77RVFEN44KQ2Y4EQJ2BC5YFCAC LF7BWEG4DKQI7NMXMZC4LC2BE5PB42HK5PD6OYBNIDMAZBJASOKQC MD3W5IRAC6UQALQE4LJC52VQNDO3I3HXF3XE2XHDABXBYJBUVAXQC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC PESSMQBJCOIA5PYNVKUG4D25VTFIG44QVCAOFRD4PKOJNW2AIHKAC JMUD7T3OJTEF5V73Y2B2GSVUBNBSJFRJPZNREBE5E4BV3IG6SJLQC 4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC 3QWK3GSA5KTVQJKXQ65OGZA2AORHFALLBXWP2A4MN6YDE7VV4PUAC PX7DDEMOBGPVK3FXKK5XEPG24CJXZSVW67DLG2JZZ5E77NVEAA3AC UEE5W7WJ46FIBN4ZH45Z33L4RYXK5AP5ZIBHYTFOJTDWVVX54QKAC FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC WKKABOJ6WDJ37ELFMS4R3OAJOSPZNZANBVWRMKBCALJKAGSPNUTAC D4FEFHQCSILZFQ5VLWNXAIRZNUMCDNGJSM4UJ6T6FDMMIWYRYILQC VNTRXQSX3XPYYZYP6ZJDIOCKQDMDBIZ2ZWRPSXPG37Q4IHCRDKIAC 66X36NZNEHWWURKSZ6G3XSC4CKQ7NTR4HYGDJGLHAUELMUAQC34AC VSBSWTE4IVQDRXLPQ7VTDIIEBEF7GMGRBHZ2IA73ZR6B2KZWI5JAC 5XQ4Y7NU63X2WW4ZR4P46LX5GEOTE7JH3AUMTDQW5VZ53GELNP2QC -- 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]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)endif line.mode == 'text' thentable.insert(event.lines, {mode='text', data=line.data, dataB=line.dataB})elseif line.mode == 'drawing' thenlocal points=deepcopy(line.points)--? print('copying', line.points, 'with', #line.points, 'points into', points)local shapes=deepcopy(line.shapes)--? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}})--? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})elseprint(line.mode)assert(false)endcurrent_drawing_mode=Drawing_mode,previous_drawing_mode=State.previous_drawing_mode,
endfunction icon.hyperlink_decoration(button_params)local x,y = button_params.x, button_params.yApp.color(Hyperlink_decoration_color)love.graphics.line(x,y+Editor_state.line_height, x+button_params.w,y+Editor_state.line_height)endfunction icon.freehand(x, y)love.graphics.line(x+4,y+7,x+5,y+5)love.graphics.line(x+5,y+5,x+7,y+4)love.graphics.line(x+7,y+4,x+9,y+3)love.graphics.line(x+9,y+3,x+10,y+5)love.graphics.line(x+10,y+5,x+12,y+6)love.graphics.line(x+12,y+6,x+13,y+8)love.graphics.line(x+13,y+8,x+13,y+10)love.graphics.line(x+13,y+10,x+14,y+12)love.graphics.line(x+14,y+12,x+15,y+14)love.graphics.line(x+15,y+14,x+15,y+16)endfunction icon.line(x, y)love.graphics.line(x+4,y+2, x+16,y+18)endfunction icon.manhattan(x, y)love.graphics.line(x+4,y+20, x+4,y+2)love.graphics.line(x+4,y+2, x+10,y+2)love.graphics.line(x+10,y+2, x+10,y+10)love.graphics.line(x+10,y+10, x+18,y+10)endfunction icon.polygon(x, y)love.graphics.line(x+8,y+2, x+14,y+2)love.graphics.line(x+14,y+2, x+18,y+10)love.graphics.line(x+18,y+10, x+10,y+18)love.graphics.line(x+10,y+18, x+4,y+12)love.graphics.line(x+4,y+12, x+8,y+2)endfunction icon.rectangle(x, y)love.graphics.line(x+4,y+8, x+4,y+16)love.graphics.line(x+4,y+16, x+16,y+16)love.graphics.line(x+16,y+16, x+16,y+8)love.graphics.line(x+16,y+8, x+4,y+8)endfunction icon.square(x, y)love.graphics.line(x+6,y+6, x+6,y+16)love.graphics.line(x+6,y+16, x+16,y+16)love.graphics.line(x+16,y+16, x+16,y+6)love.graphics.line(x+16,y+6, x+6,y+6)endfunction icon.circle(x, y)love.graphics.circle('line', x+10,y+10, 8)endendfunction icon.freehand(x, y)love.graphics.line(x+4,y+7,x+5,y+5)love.graphics.line(x+5,y+5,x+7,y+4)love.graphics.line(x+7,y+4,x+9,y+3)love.graphics.line(x+9,y+3,x+10,y+5)love.graphics.line(x+10,y+5,x+12,y+6)love.graphics.line(x+12,y+6,x+13,y+8)love.graphics.line(x+13,y+8,x+13,y+10)love.graphics.line(x+13,y+10,x+14,y+12)love.graphics.line(x+14,y+12,x+15,y+14)love.graphics.line(x+15,y+14,x+15,y+16)endfunction icon.line(x, y)love.graphics.line(x+4,y+2, x+16,y+18)endfunction icon.manhattan(x, y)love.graphics.line(x+4,y+20, x+4,y+2)love.graphics.line(x+4,y+2, x+10,y+2)love.graphics.line(x+10,y+2, x+10,y+10)love.graphics.line(x+10,y+10, x+18,y+10)endfunction icon.polygon(x, y)love.graphics.line(x+8,y+2, x+14,y+2)love.graphics.line(x+14,y+2, x+18,y+10)love.graphics.line(x+18,y+10, x+10,y+18)love.graphics.line(x+10,y+18, x+4,y+12)love.graphics.line(x+4,y+12, x+8,y+2)icon = {}function icon.insert_drawing(button_params)local x,y = button_params.x, button_params.yApp.color(Icon_color)love.graphics.rectangle('line', x,y, 12,12)love.graphics.line(4,y+6, 16,y+6)love.graphics.line(10,y, 10,y+12)
-- helpers for selecting portions of text-- To keep things simple, we'll ignore the B side when selections start on the-- A side, and stick to within a single B side selections start in.-- Return any intersection of the region from State.selection1 to State.cursor1 (or-- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and-- currently over a drawing) with the region between {line=line_index, pos=apos}-- and {line=line_index, pos=bpos}.-- apos must be less than bpos. However State.selection1 and State.cursor1 can be in any order.-- Result: positions spos,epos between apos,bpos.function Text.clip_selection(State, line_index, apos, bpos)if State.selection1.line == nil then return nil,nil end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxpif App.mouse_down(1) thenmaxl,maxp = Text.mouse_pos(State)elsemaxl,maxp = State.cursor1.line,State.cursor1.posendif Text.lt1({line=maxl, pos=maxp},{line=minl, pos=minp}) thenminl,maxl = maxl,minlminp,maxp = maxp,minpend-- check if intervals are disjointif line_index < minl then return nil,nil endif line_index > maxl then return nil,nil endif line_index == minl and bpos <= minp then return nil,nil endif line_index == maxl and apos >= maxp then return nil,nil end-- compare bounds more carefully (start inclusive, end exclusive)local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})--? print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)if a_ge and b_lt then-- fully containedreturn apos,bposelseif a_ge thenassert(maxl == line_index)return apos,maxpelseif b_lt thenassert(minl == line_index)return minp,bposelseassert(minl == maxl and minl == line_index)return minp,maxpendend-- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line-- Creates text objects every time, so use this sparingly.-- Returns some intermediate computation useful elsewhere.function Text.draw_highlight(State, line, x,y, pos, lo,hi)if lo thenlocal lo_offset = Text.offset(line.data, lo)local hi_offset = Text.offset(line.data, hi)local pos_offset = Text.offset(line.data, pos)local lo_pxif pos == lo thenlo_px = 0elselocal before = line.data:sub(pos_offset, lo_offset-1)local before_text = App.newText(love.graphics.getFont(), before)lo_px = App.width(before_text)end--? print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px)local s = line.data:sub(lo_offset, hi_offset-1)local text = App.newText(love.graphics.getFont(), s)local text_width = App.width(text)App.color(Highlight_color)love.graphics.rectangle('fill', x+lo_px,y, text_width,State.line_height)App.color(Text_color)return lo_pxendend-- inefficient for some reason, so don't do it on every framefunction Text.mouse_pos(State)local time = love.timer.getTime()if State.recent_mouse.time and State.recent_mouse.time > time-0.1 thenreturn State.recent_mouse.line, State.recent_mouse.posendState.recent_mouse.time = timelocal line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())if line thenState.recent_mouse.line = lineState.recent_mouse.pos = posendreturn State.recent_mouse.line, State.recent_mouse.posendfunction Text.to_pos(State, x,y)for line_index,line in ipairs(State.lines) doif line.mode == 'text' thenif Text.in_line(State, line_index, x,y) thenreturn line_index, Text.to_pos_on_line(State, line_index, x,y)endendendendfunction Text.cut_selection(State)if State.selection1.line == nil then return endlocal result = Text.selection(State)Text.delete_selection(State)return resultendfunction Text.delete_selection(State)if State.selection1.line == nil then return endlocal minl,maxl = minmax(State.selection1.line, State.cursor1.line)local before = snapshot(State, minl, maxl)Text.delete_selection_without_undo(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endfunction Text.delete_selection_without_undo(State)if State.selection1.line == nil then return end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxp = State.cursor1.line,State.cursor1.posif minl > maxl thenminl,maxl = maxl,minlminp,maxp = maxp,minpelseif minl == maxl thenif minp > maxp thenminp,maxp = maxp,minpendend-- update State.cursor1 and State.selection1State.cursor1.line = minlState.cursor1.pos = minpif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1.line = State.cursor1.lineState.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)endState.selection1 = {}-- delete everything between min (inclusive) and max (exclusive)Text.clear_screen_line_cache(State, minl)local min_offset = Text.offset(State.lines[minl].data, minp)local max_offset = Text.offset(State.lines[maxl].data, maxp)if minl == maxl then--? print('minl == maxl')State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)returnendassert(minl < maxl)local rhs = State.lines[maxl].data:sub(max_offset)for i=maxl,minl+1,-1 dotable.remove(State.lines, i)table.remove(State.line_cache, i)endState.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhsendfunction Text.selection(State)if State.selection1.line == nil then return end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxp = State.cursor1.line,State.cursor1.posif minl > maxl thenminl,maxl = maxl,minlminp,maxp = maxp,minpelseif minl == maxl thenif minp > maxp thenminp,maxp = maxp,minpendendlocal min_offset = Text.offset(State.lines[minl].data, minp)local max_offset = Text.offset(State.lines[maxl].data, maxp)if minl == maxl thenreturn State.lines[minl].data:sub(min_offset, max_offset-1)endassert(minl < maxl)local result = {State.lines[minl].data:sub(min_offset)}for i=minl+1,maxl-1 doif State.lines[i].mode == 'text' thentable.insert(result, State.lines[i].data)endendtable.insert(result, State.lines[maxl].data:sub(1, max_offset-1))return table.concat(result, '\n')end
function draw_help_without_mouse_pressed(State, drawing_index)local drawing = State.lines[drawing_index]local line_cache = State.line_cache[drawing_index]App.color(Help_color)local y = line_cache.starty+10love.graphics.print("Things you can do:", State.left+30,y)y = y + State.line_heightlove.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y)y = y + State.line_heightlove.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", State.left+30,y)y = y + State.line_heightlove.graphics.print("then press the mouse button to drop it", State.left+30+bullet_indent(),y)y = y + State.line_heightlove.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", State.left+30,y)y = y + State.line_heightlove.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", State.left+30,y)y = y + State.line_heightif State.current_drawing_mode ~= 'freehand' thenlove.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'line' thenlove.graphics.print("* Press 'ctrl+l' to switch to drawing lines", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'manhattan' thenlove.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'circle' thenlove.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'polygon' thenlove.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'rectangle' thenlove.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'square' thenlove.graphics.print("* Press 'ctrl+s' to switch to drawing squares", State.left+30,y)y = y + State.line_heightendlove.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", State.left+30,y)y = y + State.line_heightlove.graphics.print("Press 'esc' now to hide this message", State.left+30,y)y = y + State.line_heightApp.color(Help_background_color)love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))endfunction draw_help_with_mouse_pressed(State, drawing_index)local drawing = State.lines[drawing_index]local line_cache = State.line_cache[drawing_index]App.color(Help_color)local y = line_cache.starty+10love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y)y = y + State.line_heightlove.graphics.print('Things you can do now:', State.left+30,y)y = y + State.line_heightif State.current_drawing_mode == 'freehand' thenlove.graphics.print('* Release the mouse button to finish drawing the stroke', State.left+30,y)y = y + State.line_heightelseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' thenlove.graphics.print('* Release the mouse button to finish drawing the line', State.left+30,y)y = y + State.line_heightelseif State.current_drawing_mode == 'circle' thenif drawing.pending.mode == 'circle' thenlove.graphics.print('* Release the mouse button to finish drawing the circle', State.left+30,y)y = y + State.line_heightlove.graphics.print("* Press 'a' to draw just an arc of a circle", State.left+30,y)elselove.graphics.print('* Release the mouse button to finish drawing the arc', State.left+30,y)endy = y + State.line_heightelseif State.current_drawing_mode == 'polygon' thenlove.graphics.print('* Release the mouse button to finish drawing the polygon', State.left+30,y)y = y + State.line_heightlove.graphics.print("* Press 'p' to add a vertex to the polygon", State.left+30,y)y = y + State.line_heightelseif State.current_drawing_mode == 'rectangle' thenif #drawing.pending.vertices < 2 thenlove.graphics.print("* Press 'p' to add a vertex to the rectangle", State.left+30,y)y = y + State.line_heightelselove.graphics.print('* Release the mouse button to finish drawing the rectangle', State.left+30,y)y = y + State.line_heightlove.graphics.print("* Press 'p' to replace the second vertex of the rectangle", State.left+30,y)y = y + State.line_heightendelseif State.current_drawing_mode == 'square' thenif #drawing.pending.vertices < 2 thenlove.graphics.print("* Press 'p' to add a vertex to the square", State.left+30,y)y = y + State.line_heightelselove.graphics.print('* Release the mouse button to finish drawing the square', State.left+30,y)y = y + State.line_heightlove.graphics.print("* Press 'p' to replace the second vertex of the square", State.left+30,y)y = y + State.line_heightendendlove.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", State.left+30,y)y = y + State.line_heighty = y + State.line_heightif State.current_drawing_mode ~= 'line' thenlove.graphics.print("* Press 'l' to switch to drawing lines", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'manhattan' thenlove.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'circle' thenlove.graphics.print("* Press 'o' to switch to drawing circles/arcs", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'polygon' thenlove.graphics.print("* Press 'g' to switch to drawing polygons", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'rectangle' thenlove.graphics.print("* Press 'r' to switch to drawing rectangles", State.left+30,y)y = y + State.line_heightendif State.current_drawing_mode ~= 'square' thenlove.graphics.print("* Press 's' to switch to drawing squares", State.left+30,y)y = y + State.line_heightendApp.color(Help_background_color)love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty))endfunction current_shape(State, shape)if State.current_drawing_mode == 'freehand' thenreturn 'freehand stroke'elseif State.current_drawing_mode == 'line' thenreturn 'straight line'elseif State.current_drawing_mode == 'manhattan' thenreturn 'horizontal/vertical line'elseif State.current_drawing_mode == 'circle' and shape and shape.start_angle thenreturn 'arc'elsereturn State.current_drawing_modeendendfunction bullet_indent()return App.width(to_text('* '))end
geom = {}function geom.on_shape(x,y, drawing, shape)if shape.mode == 'freehand' thenreturn geom.on_freehand(x,y, drawing, shape)elseif shape.mode == 'line' thenreturn geom.on_line(x,y, drawing, shape)elseif shape.mode == 'manhattan' thenlocal p1 = drawing.points[shape.p1]local p2 = drawing.points[shape.p2]if p1.x == p2.x thenif x ~= p1.x then return false endlocal y1,y2 = p1.y, p2.yif y1 > y2 theny1,y2 = y2,y1endreturn y >= y1-2 and y <= y2+2elseif p1.y == p2.y thenif y ~= p1.y then return false endlocal x1,x2 = p1.x, p2.xif x1 > x2 thenx1,x2 = x2,x1endreturn x >= x1-2 and x <= x2+2endelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenreturn geom.on_polygon(x,y, drawing, shape)elseif shape.mode == 'circle' thenlocal center = drawing.points[shape.center]local dist = geom.dist(center.x,center.y, x,y)return dist > shape.radius*0.95 and dist < shape.radius*1.05elseif shape.mode == 'arc' thenlocal center = drawing.points[shape.center]local dist = geom.dist(center.x,center.y, x,y)if dist < shape.radius*0.95 or dist > shape.radius*1.05 thenreturn falseendreturn geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle)elseif shape.mode == 'deleted' thenelseprint(shape.mode)assert(false)endendfunction geom.on_freehand(x,y, drawing, shape)local prevfor _,p in ipairs(shape.points) doif prev thenif geom.on_line(x,y, drawing, {p1=prev, p2=p}) thenreturn trueendendprev = pendreturn falseendfunction geom.on_line(x,y, drawing, shape)local p1,p2if type(shape.p1) == 'number' thenp1 = drawing.points[shape.p1]p2 = drawing.points[shape.p2]elsep1 = shape.p1p2 = shape.p2endif p1.x == p2.x thenif math.abs(p1.x-x) > 2 thenreturn falseendlocal y1,y2 = p1.y,p2.yif y1 > y2 theny1,y2 = y2,y1endreturn y >= y1-2 and y <= y2+2end-- has the right slope and interceptlocal m = (p2.y - p1.y) / (p2.x - p1.x)local yp = p1.y + m*(x-p1.x)if yp < y-2 or yp > y+2 thenreturn falseend-- between endpointslocal k = (x-p1.x) / (p2.x-p1.x)return k > -0.005 and k < 1.005endfunction geom.on_polygon(x,y, drawing, shape)local prevfor _,p in ipairs(shape.vertices) doif prev thenif geom.on_line(x,y, drawing, {p1=prev, p2=p}) thenreturn trueendendprev = pendreturn geom.on_line(x,y, drawing, {p1=shape.vertices[1], p2=shape.vertices[#shape.vertices]})end-- are (x3,y3) and (x4,y4) on the same side of the line between (x1,y1) and (x2,y2)function geom.same_side(x1,y1, x2,y2, x3,y3, x4,y4)if x1 == x2 thenreturn math.sign(x3-x1) == math.sign(x4-x1)endif y1 == y2 thenreturn math.sign(y3-y1) == math.sign(y4-y1)endlocal m = (y2-y1)/(x2-x1)return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4)endfunction math.sign(x)if x > 0 thenreturn 1elseif x == 0 thenreturn 0elseif x < 0 thenreturn -1endendfunction geom.angle_with_hint(x1, y1, x2, y2, hint)local result = geom.angle(x1,y1, x2,y2)if hint then-- Smooth the discontinuity where angle goes from positive to negative.-- The hint is a memory of which way we drew it last time.while result > hint+math.pi/10 doresult = result-math.pi*2endwhile result < hint-math.pi/10 doresult = result+math.pi*2endendreturn resultend-- result is from -π/2 to 3π/2, approximately adding math.atan2 from Lua 5.3-- (LÖVE is Lua 5.1)function geom.angle(x1,y1, x2,y2)local result = math.atan((y2-y1)/(x2-x1))if x2 < x1 thenresult = result+math.piendreturn resultend-- is the line between x,y and cx,cy at an angle between s and e?function geom.angle_between(ox,oy, x,y, s,e)local angle = geom.angle(ox,oy, x,y)if s > e thens,e = e,send-- I'm not sure this is right or ideal..angle = angle-math.pi*2if s <= angle and angle <= e thenreturn trueendangle = angle+math.pi*2if s <= angle and angle <= e thenreturn trueendangle = angle+math.pi*2return s <= angle and angle <= eendfunction geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 end
-- major tests for drawings-- We minimize assumptions about specific pixels, and try to test at the level-- of specific shapes. In particular, no tests of freehand drawings.function test_creating_drawing_saves()io.write('\ntest_creating_drawing_saves')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{}Text.redraw_all(Editor_state)edit.draw(Editor_state)-- click on button to create drawingedit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1)-- file not immediately savededit.update(Editor_state, 0.01)check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- filesystem contains drawing and an empty line of textcheck_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves')endfunction test_draw_line()io.write('\ntest_draw_line')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_draw_line/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_line/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_line/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_line/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_line/baseline/#shapes')-- draw a lineedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_line/#shapes')check_eq(#drawing.points, 2, 'F - test_draw_line/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_draw_line/p1:x')check_eq(p1.y, 6, 'F - test_draw_line/p1:y')check_eq(p2.x, 35, 'F - test_draw_line/p2:x')check_eq(p2.y, 36, 'F - test_draw_line/p2:y')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- The format on disk isn't perfectly stable. Table fields can be reordered.-- So just reload from disk to verify.load_from_disk(Editor_state)Text.redraw_all(Editor_state)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_line/save/#shapes')check_eq(#drawing.points, 2, 'F - test_draw_line/save/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/save/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_draw_line/save/p1:x')check_eq(p1.y, 6, 'F - test_draw_line/save/p1:y')check_eq(p2.x, 35, 'F - test_draw_line/save/p2:x')check_eq(p2.y, 36, 'F - test_draw_line/save/p2:y')endfunction test_draw_horizontal_line()io.write('\ntest_draw_horizontal_line')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'manhattan'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_draw_horizontal_line/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_horizontal_line/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_horizontal_line/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_horizontal_line/baseline/#shapes')-- draw a line that is more horizontal than verticaledit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_horizontal_line/#shapes')check_eq(#drawing.points, 2, 'F - test_draw_horizontal_line/#points')check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_draw_horizontal_line/shape_mode')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_draw_horizontal_line/p1:x')check_eq(p1.y, 6, 'F - test_draw_horizontal_line/p1:y')check_eq(p2.x, 35, 'F - test_draw_horizontal_line/p2:x')check_eq(p2.y, p1.y, 'F - test_draw_horizontal_line/p2:y')endfunction test_draw_circle()io.write('\ntest_draw_circle')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_draw_circle/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle/baseline/#shapes')-- draw a circleApp.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawingedit.run_after_keychord(Editor_state, 'C-o')edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_circle/#shapes')check_eq(#drawing.points, 1, 'F - test_draw_circle/#points')check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle/radius')local center = drawing.points[drawing.shapes[1].center]check_eq(center.x, 35, 'F - test_draw_circle/center:x')check_eq(center.y, 36, 'F - test_draw_circle/center:y')endfunction test_cancel_stroke()io.write('\ntest_cancel_stroke')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_cancel_stroke/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_cancel_stroke/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_cancel_stroke/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_cancel_stroke/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_cancel_stroke/baseline/#shapes')-- start drawing a lineedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)-- canceledit.run_after_keychord(Editor_state, 'escape')edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes')endfunction test_keys_do_not_affect_shape_when_mouse_up()io.write('\ntest_keys_do_not_affect_shape_when_mouse_up')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)-- hover over drawing and press 'o' without holding mouseApp.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawingedit.run_after_keychord(Editor_state, 'o')-- no change to drawing modecheck_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode')-- no change to text either because we didn't run the textinput eventendfunction test_draw_circle_mid_stroke()io.write('\ntest_draw_circle_mid_stroke')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes')-- draw a circleApp.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawingedit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_keychord(Editor_state, 'o')edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes')check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points')check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode')check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius')local center = drawing.points[drawing.shapes[1].center]check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x')check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y')endfunction test_draw_arc()io.write('\ntest_draw_arc')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'circle'edit.draw(Editor_state)check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes')-- draw an arcedit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)App.mouse_move(Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'a') -- arc modeedit.run_after_mouse_release(Editor_state, Editor_state.left+35+50, Editor_state.top+Drawing_padding_top+36+50, 1) -- 45°local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes')check_eq(#drawing.points, 1, 'F - test_draw_arc/#points')check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode')local arc = drawing.shapes[1]check_eq(arc.radius, 30, 'F - test_draw_arc/radius')local center = drawing.points[arc.center]check_eq(center.x, 35, 'F - test_draw_arc/center:x')check_eq(center.y, 36, 'F - test_draw_arc/center:y')check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle')check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle')endfunction test_draw_polygon()io.write('\ntest_draw_polygon')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)edit.draw(Editor_state)check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode')check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes')-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_keychord(Editor_state, 'g') -- polygon mode-- second pointApp.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'p') -- add point-- final pointedit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes')check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices')local shape = drawing.shapes[1]check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode')check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices')local p = drawing.points[shape.vertices[1]]check_eq(p.x, 5, 'F - test_draw_polygon/p1:x')check_eq(p.y, 6, 'F - test_draw_polygon/p1:y')local p = drawing.points[shape.vertices[2]]check_eq(p.x, 65, 'F - test_draw_polygon/p2:x')check_eq(p.y, 36, 'F - test_draw_polygon/p2:y')local p = drawing.points[shape.vertices[3]]check_eq(p.x, 35, 'F - test_draw_polygon/p3:x')check_eq(p.y, 26, 'F - test_draw_polygon/p3:y')endfunction test_draw_rectangle()io.write('\ntest_draw_rectangle')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)edit.draw(Editor_state)check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode')check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes')-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_keychord(Editor_state, 'r') -- rectangle mode-- second point/first edgeApp.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)edit.run_after_keychord(Editor_state, 'p')-- override second point/first edgeApp.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)edit.run_after_keychord(Editor_state, 'p')-- release (decides 'thickness' of rectangle perpendicular to first edge)edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes')check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point addedlocal shape = drawing.shapes[1]check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode')check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices')local p = drawing.points[shape.vertices[1]]check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x')check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y')local p = drawing.points[shape.vertices[2]]check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x')check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y')local p = drawing.points[shape.vertices[3]]check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x')check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y')local p = drawing.points[shape.vertices[4]]check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x')check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y')endfunction test_draw_rectangle_intermediate()io.write('\ntest_draw_rectangle_intermediate')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)edit.draw(Editor_state)check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode')check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes')-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_keychord(Editor_state, 'r') -- rectangle mode-- second point/first edgeApp.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)edit.run_after_keychord(Editor_state, 'p')-- override second point/first edgeApp.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76)edit.run_after_keychord(Editor_state, 'p')local drawing = Editor_state.lines[1]check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point addedlocal pending = drawing.pendingcheck_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode')check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices')local p = drawing.points[pending.vertices[1]]check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x')check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y')local p = drawing.points[pending.vertices[2]]check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x')check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y')-- outline of rectangle is drawn based on where the mouse is, but we can't check that so farendfunction test_draw_square()io.write('\ntest_draw_square')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)edit.draw(Editor_state)check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode')check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines')check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode')check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y')check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y')check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes')-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_keychord(Editor_state, 's') -- square mode-- second point/first edgeApp.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45)edit.run_after_keychord(Editor_state, 'p')-- override second point/first edgeApp.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66)edit.run_after_keychord(Editor_state, 'p')-- release (decides which side of first edge to draw square on)edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes')check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point addedcheck_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode')check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices')local p = drawing.points[drawing.shapes[1].vertices[1]]check_eq(p.x, 35, 'F - test_draw_square/p1:x')check_eq(p.y, 36, 'F - test_draw_square/p1:y')local p = drawing.points[drawing.shapes[1].vertices[2]]check_eq(p.x, 65, 'F - test_draw_square/p2:x')check_eq(p.y, 66, 'F - test_draw_square/p2:y')local p = drawing.points[drawing.shapes[1].vertices[3]]check_eq(p.x, 35, 'F - test_draw_square/p3:x')check_eq(p.y, 96, 'F - test_draw_square/p3:y')local p = drawing.points[drawing.shapes[1].vertices[4]]check_eq(p.x, 5, 'F - test_draw_square/p4:x')check_eq(p.y, 66, 'F - test_draw_square/p4:y')endfunction test_name_point()io.write('\ntest_name_point')-- create a drawing with a lineApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)-- draw a lineedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes')check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x')check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y')check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x')check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y')check_nil(p2.name, 'F - test_name_point/baseline/p2:name')-- enter 'name' mode without moving the mouseedit.run_after_keychord(Editor_state, 'C-n')check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1')edit.run_after_textinput(Editor_state, 'A')check_eq(p2.name, 'A', 'F - test_name_point')-- still in 'name' modecheck_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2')-- exit 'name' modeedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3')check_eq(p2.name, 'A', 'F - test_name_point')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- change is savedload_from_disk(Editor_state)Text.redraw_all(Editor_state)local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]check_eq(p2.name, 'A', 'F - test_name_point/save')endfunction test_move_point()io.write('\ntest_move_point')-- create a drawing with a lineApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes')check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x')check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y')check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x')check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- line is saved to diskload_from_disk(Editor_state)Text.redraw_all(Editor_state)local drawing = Editor_state.lines[1]local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]check_eq(p2.x, 35, 'F - test_move_point/save/x')check_eq(p2.y, 36, 'F - test_move_point/save/y')edit.draw(Editor_state)-- enter 'move' mode without moving the mouseedit.run_after_keychord(Editor_state, 'C-u')check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point/mode:1')-- point is liftedcheck_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2')check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target')-- move pointApp.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)edit.update(Editor_state, 0.05)local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p2.x, 26, 'F - test_move_point/x')check_eq(p2.y, 44, 'F - test_move_point/y')-- exit 'move' modeedit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_move_point/mode:3')check_eq(drawing.pending, {}, 'F - test_move_point/pending')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- change is savedload_from_disk(Editor_state)Text.redraw_all(Editor_state)local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]check_eq(p2.x, 26, 'F - test_move_point/save/x')check_eq(p2.y, 44, 'F - test_move_point/save/y')endfunction test_move_point_on_manhattan_line()io.write('\ntest_move_point_on_manhattan_line')-- create a drawing with a manhattan lineApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'manhattan'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+46, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes')check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points')check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1')edit.draw(Editor_state)-- enter 'move' modeedit.run_after_keychord(Editor_state, 'C-u')check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1')-- move pointApp.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)edit.update(Editor_state, 0.05)-- line is no longer manhattancheck_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1')endfunction test_delete_lines_at_point()io.write('\ntest_delete_lines_at_point')-- create a drawing with two lines connected at a pointApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes')check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1')check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2')-- hover on the common point and deleteApp.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'C-d')check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1')check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2')-- wait for some timeApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- deleted points disappear after file is reloadedload_from_disk(Editor_state)Text.redraw_all(Editor_state)check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save')endfunction test_delete_line_under_mouse_pointer()io.write('\ntest_delete_line_under_mouse_pointer')-- create a drawing with two lines connected at a pointApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes')check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1')check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2')-- hover on one of the lines and deleteApp.mouse_move(Editor_state.left+25, Editor_state.top+Drawing_padding_top+26)edit.run_after_keychord(Editor_state, 'C-d')-- only that line is deletedcheck_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1')check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2')endfunction test_delete_point_from_polygon()io.write('\ntest_delete_point_from_polygon')-- create a drawing with two lines connected at a pointApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_keychord(Editor_state, 'g') -- polygon mode-- second pointApp.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'p') -- add point-- third pointApp.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)edit.run_after_keychord(Editor_state, 'p') -- add point-- fourth pointedit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices')-- hover on a point and deleteApp.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26)edit.run_after_keychord(Editor_state, 'C-d')-- just the one point is deletedcheck_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape')check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices')endfunction test_delete_point_from_polygon()io.write('\ntest_delete_point_from_polygon')-- create a drawing with two lines connected at a pointApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)-- first pointedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_keychord(Editor_state, 'g') -- polygon mode-- second pointApp.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'p') -- add point-- third pointedit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes')check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode')check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices')-- hover on a point and deleteApp.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'C-d')-- there's < 3 points left, so the whole polygon is deletedcheck_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon')endfunction test_undo_name_point()io.write('\ntest_undo_name_point')-- create a drawing with a lineApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)-- draw a lineedit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes')check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x')check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y')check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x')check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y')check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name')check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1')--? print('a', Editor_state.lines.current_drawing)-- enter 'name' mode without moving the mouseedit.run_after_keychord(Editor_state, 'C-n')edit.run_after_textinput(Editor_state, 'A')edit.run_after_keychord(Editor_state, 'return')check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline')check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2')check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history')--? print('b', Editor_state.lines.current_drawing)-- undoedit.run_after_keychord(Editor_state, 'C-z')local drawing = Editor_state.lines[1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history')check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- undo is savedload_from_disk(Editor_state)Text.redraw_all(Editor_state)local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]check_eq(p2.name, '', 'F - test_undo_name_point/save')endfunction test_undo_move_point()io.write('\ntest_undo_move_point')-- create a drawing with a lineApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes')check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points')check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1')local p1 = drawing.points[drawing.shapes[1].p1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x')check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y')check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x')check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y')check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name')-- move p2edit.run_after_keychord(Editor_state, 'C-u')App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44)edit.update(Editor_state, 0.05)local p2 = drawing.points[drawing.shapes[1].p2]check_eq(p2.x, 26, 'F - test_undo_move_point/x')check_eq(p2.y, 44, 'F - test_undo_move_point/y')-- exit 'move' modeedit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1)check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history')-- undoedit.run_after_keychord(Editor_state, 'C-z')edit.run_after_keychord(Editor_state, 'C-z') -- bug: need to undo twicelocal drawing = Editor_state.lines[1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history')check_eq(p2.x, 35, 'F - test_undo_move_point/x')check_eq(p2.y, 36, 'F - test_undo_move_point/y')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- undo is savedload_from_disk(Editor_state)Text.redraw_all(Editor_state)local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2]check_eq(p2.x, 35, 'F - test_undo_move_point/save/x')check_eq(p2.y, 36, 'F - test_undo_move_point/save/y')endfunction test_undo_delete_point()io.write('\ntest_undo_delete_point')-- create a drawing with two lines connected at a pointApp.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixelsEditor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)Editor_state.current_drawing_mode = 'line'edit.draw(Editor_state)edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1)local drawing = Editor_state.lines[1]check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes')check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1')check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2')-- hover on the common point and deleteApp.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36)edit.run_after_keychord(Editor_state, 'C-d')check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1')check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2')-- undoedit.run_after_keychord(Editor_state, 'C-z')local drawing = Editor_state.lines[1]local p2 = drawing.points[drawing.shapes[1].p2]check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history')check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1')check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2')-- wait until saveApp.wait_fake_time(3.1)edit.update(Editor_state, 0)-- undo is savedload_from_disk(Editor_state)Text.redraw_all(Editor_state)check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save')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_cachecheck_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 mouse-- 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()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')endEditor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'}function test_undo_restores_selection()io.write('\ntest_undo_restores_selection')-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- delete selected textedit.run_after_textinput(Editor_state, 'x')check_eq(Editor_state.lines[1].data, 'xbc', 'F - test_undo_restores_selection/baseline')check_nil(Editor_state.selection1.line, 'F - test_undo_restores_selection/baseline:selection')-- undoedit.run_after_keychord(Editor_state, 'C-z')edit.run_after_keychord(Editor_state, 'C-z')-- selection is restoredcheck_eq(Editor_state.selection1.line, 1, 'F - test_undo_restores_selection/line')check_eq(Editor_state.selection1.pos, 2, 'F - test_undo_restores_selection/pos')endcheck_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/selection:line')check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/selection:pos')--? check_eq(Editor_state.selection1.line, 2, 'F - test_undo_delete_text/selection:line')--? check_eq(Editor_state.selection1.pos, 4, 'F - test_undo_delete_text/selection:pos')check_nil(Editor_state.selection1.line, 'F - test_undo_delete_text/baseline/selection:line')check_nil(Editor_state.selection1.pos, 'F - test_undo_delete_text/baseline/selection:pos')check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/selection:line')check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/selection:pos')check_nil(Editor_state.selection1.line, 'F - test_undo_insert_text/baseline/selection:line')check_nil(Editor_state.selection1.pos, 'F - test_undo_insert_text/baseline/selection:pos')-- some tests for operating over selections created using Shift- chords-- we're just testing delete_selection, and it works the same for all keysfunction test_backspace_over_selection()io.write('\ntest_backspace_over_selection')-- select just one character within a line with cursor before selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}-- backspace deletes the selected character, even though it's after the cursoredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection/data")-- cursor (remains) at start of selectioncheck_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection/cursor:line")check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection/cursor:pos")-- selection is clearedcheck_nil(Editor_state.selection1.line, "F - test_backspace_over_selection/selection")endfunction test_backspace_over_selection_reverse()io.write('\ntest_backspace_over_selection_reverse')-- select just one character within a line with cursor after selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.selection1 = {line=1, pos=1}-- backspace deletes the selected characteredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'bc', "F - test_backspace_over_selection_reverse/data")-- cursor moves to start of selectioncheck_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_selection_reverse/cursor:line")check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_over_selection_reverse/cursor:pos")-- selection is clearedcheck_nil(Editor_state.selection1.line, "F - test_backspace_over_selection_reverse/selection")endfunction test_backspace_over_multiple_lines()io.write('\ntest_backspace_over_multiple_lines')-- select just one character within a line with cursor after selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.selection1 = {line=4, pos=2}-- backspace deletes the region and joins the remaining portions of lines on either sideedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'akl', "F - test_backspace_over_multiple_lines/data:1")check_eq(Editor_state.lines[2].data, 'mno', "F - test_backspace_over_multiple_lines/data:2")-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 1, "F - test_backspace_over_multiple_lines/cursor:line")check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_over_multiple_lines/cursor:pos")-- selection is clearedcheck_nil(Editor_state.selection1.line, "F - test_backspace_over_multiple_lines/selection")endfunction test_backspace_to_end_of_line()io.write('\ntest_backspace_to_end_of_line')-- select region from cursor to end of 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=1, pos=2}Editor_state.selection1 = {line=1, pos=4}-- backspace deletes rest of line without joining to any other lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'a', "F - test_backspace_to_start_of_line/data:1")check_eq(Editor_state.lines[2].data, 'def', "F - test_backspace_to_start_of_line/data:2")-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 1, "F - test_backspace_to_start_of_line/cursor:line")check_eq(Editor_state.cursor1.pos, 2, "F - test_backspace_to_start_of_line/cursor:pos")-- selection is clearedcheck_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")endfunction test_backspace_to_start_of_line()io.write('\ntest_backspace_to_start_of_line')-- select region from cursor to start of 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.selection1 = {line=2, pos=3}-- backspace deletes beginning of line without joining to any other lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abc', "F - test_backspace_to_start_of_line/data:1")check_eq(Editor_state.lines[2].data, 'f', "F - test_backspace_to_start_of_line/data:2")-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 2, "F - test_backspace_to_start_of_line/cursor:line")check_eq(Editor_state.cursor1.pos, 1, "F - test_backspace_to_start_of_line/cursor:pos")-- selection is clearedcheck_nil(Editor_state.selection1.line, "F - test_backspace_to_start_of_line/selection")endedit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)endfunction test_pagedown_skips_drawings()io.write('\ntest_pagedown_skips_drawings')-- some lines of text with a drawing intermixedlocal drawing_width = 50App.screen.init{width=Editor_state.left+drawing_width, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', -- height 15'```lines', '```', -- height 25'def', -- height 15'ghi'} -- height 15Text.redraw_all(Editor_state)check_eq(Editor_state.lines[2].mode, 'drawing', 'F - test_pagedown_skips_drawings/baseline/lines')Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}local drawing_height = Drawing_padding_height + drawing_width/2 -- default-- initially the screen displays the first line and the drawing-- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80pxedit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_pagedown_skips_drawings/baseline/screen:1')-- after pagedown the screen draws the drawing up top-- 15px margin + 10px margin + 25px drawing + 10px margin + 15px line3 = 75px < screen height 80pxedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown_skips_drawings/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_skips_drawings/cursor')y = Editor_state.top + drawing_heightApp.screen.check(y, 'def', 'F - test_pagedown_skips_drawings/screen:1')function test_cut_without_selection()io.write('\ntest_cut_without_selection')-- 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 = {}Editor_state.selection1 = {}edit.draw(Editor_state)-- try to cut without selecting textedit.run_after_keychord(Editor_state, 'C-x')-- no crashcheck_nil(Editor_state.selection1.line, 'F - test_cut_without_selection')endcheck_nil(Editor_state.selection1.line, 'F - test_move_cursor_using_mouse/selection:line')check_nil(Editor_state.selection1.pos, 'F - test_move_cursor_using_mouse/selection:pos')endfunction test_select_text_using_mouse()io.write('\ntest_select_text_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 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- press and hold on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- drag and release somewhere elseedit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_using_mouse/selection:line')check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_using_mouse/selection:pos')check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_using_mouse/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_select_text_using_mouse/cursor:pos')endfunction test_select_text_using_mouse_and_shift()io.write('\ntest_select_text_using_mouse_and_shift')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- click on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- hold down shift and click somewhere elseApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)App.fake_key_release('lshift')check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_using_mouse_and_shift/selection:line')check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_using_mouse_and_shift/selection:pos')check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_using_mouse_and_shift/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_select_text_using_mouse_and_shift/cursor:pos')endfunction test_select_text_repeatedly_using_mouse_and_shift()io.write('\ntest_select_text_repeatedly_using_mouse_and_shift')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)Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- click on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- hold down shift and click on a second locationApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)-- hold down shift and click at a third locationApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height+5, 1)App.fake_key_release('lshift')-- selection is between first and third location. forget the second location, not the first.check_eq(Editor_state.selection1.line, 1, 'F - test_select_text_repeatedly_using_mouse_and_shift/selection:line')check_eq(Editor_state.selection1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/selection:pos')check_eq(Editor_state.cursor1.line, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text_repeatedly_using_mouse_and_shift/cursor:pos')edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)Editor_state.selection1 = {}function test_edit_deletes_selection()io.write('\ntest_edit_deletes_selection')-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press a keyedit.run_after_textinput(Editor_state, 'x')-- selected text is deleted and replaced with the keycheck_eq(Editor_state.lines[1].data, 'xbc', 'F - test_edit_deletes_selection')endfunction test_edit_with_shift_key_deletes_selection()io.write('\ntest_edit_with_shift_key_deletes_selection')-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- mimic precise keypresses for a capital letterApp.fake_key_press('lshift')edit.keychord_pressed(Editor_state, 'd', 'd')edit.textinput(Editor_state, 'D')edit.key_released(Editor_state, 'd')App.fake_key_release('lshift')-- selected text is deleted and replaced with the keycheck_nil(Editor_state.selection1.line, 'F - test_edit_with_shift_key_deletes_selection')check_eq(Editor_state.lines[1].data, 'Dbc', 'F - test_edit_with_shift_key_deletes_selection/data')endfunction test_copy_does_not_reset_selection()io.write('\ntest_copy_does_not_reset_selection')-- display a line of text with a selectionApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- copy selectionedit.run_after_keychord(Editor_state, 'C-c')check_eq(App.clipboard, 'a', 'F - test_copy_does_not_reset_selection/clipboard')-- selection is reset since shift key is not pressedcheck(Editor_state.selection1.line, 'F - test_copy_does_not_reset_selection')endfunction test_cut()io.write('\ntest_cut')-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press a keyedit.run_after_keychord(Editor_state, 'C-x')check_eq(App.clipboard, 'a', 'F - test_cut/clipboard')-- selected text is deletedcheck_eq(Editor_state.lines[1].data, 'bc', 'F - test_cut/data')endfunction test_paste_replaces_selection()io.write('\ntest_paste_replaces_selection')-- display a line of text with a selectionApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.selection1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- set clipboardApp.clipboard = 'xyz'-- paste selectionedit.run_after_keychord(Editor_state, 'C-v')-- selection is reset since shift key is not pressed-- selection includes the newline, so it's also deletedcheck_eq(Editor_state.lines[1].data, 'xyzdef', 'F - test_paste_replaces_selection')endfunction test_deleting_selection_may_scroll()io.write('\ntest_deleting_selection_may_scroll')-- display lines 2/3/4App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=2}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'F - test_deleting_selection_may_scroll/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_deleting_selection_may_scroll/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_deleting_selection_may_scroll/baseline/screen:3')-- set up a selection starting above the currently displayed pageEditor_state.selection1 = {line=1, pos=2}-- delete selectionedit.run_after_keychord(Editor_state, 'backspace')-- page scrolls upcheck_eq(Editor_state.screen_top1.line, 1, 'F - test_deleting_selection_may_scroll')check_eq(Editor_state.lines[1].data, 'ahi', 'F - test_deleting_selection_may_scroll/data')endendfunction test_select_text()io.write('\ntest_select_text')-- display a line of textApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- select a letterApp.fake_key_press('lshift')edit.run_after_keychord(Editor_state, 'S-right')App.fake_key_release('lshift')edit.key_released(Editor_state, 'lshift')-- selection persists even after shift is releasedcheck_eq(Editor_state.selection1.line, 1, 'F - test_select_text/selection:line')check_eq(Editor_state.selection1.pos, 1, 'F - test_select_text/selection:pos')check_eq(Editor_state.cursor1.line, 1, 'F - test_select_text/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_select_text/cursor:pos')endfunction test_cursor_movement_without_shift_resets_selection()io.write('\ntest_cursor_movement_without_shift_resets_selection')-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press an arrow key without shiftedit.run_after_keychord(Editor_state, 'right')-- no change to data, selection is resetcheck_nil(Editor_state.selection1.line, 'F - test_cursor_movement_without_shift_resets_selection')check_eq(Editor_state.lines[1].data, 'abc', 'F - test_cursor_movement_without_shift_resets_selection/data')check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/selection is empty to avoid perturbing future edits')check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_on_wrapping_line/selection is empty to avoid perturbing future edits')check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_takes_margins_into_account/selection is empty to avoid perturbing future edits')check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse_to_left_of_line/selection is empty to avoid perturbing future edits')check_nil(Editor_state.selection1.line, 'F - test_click_with_mouse/selection is empty to avoid perturbing future edits')endfunction test_click_to_create_drawing()io.write('\ntest_click_to_create_drawing')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_mouse_click(Editor_state, 8,Editor_state.top+8, 1)-- cursor skips drawing to always remain on textcheck_eq(#Editor_state.lines, 2, 'F - test_click_to_create_drawing/#lines')check_eq(Editor_state.cursor1.line, 2, 'F - test_click_to_create_drawing/cursor')endfunction test_backspace_to_delete_drawing()io.write('\ntest_backspace_to_delete_drawing')-- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end)App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'```lines', '```', ''}Text.redraw_all(Editor_state)-- cursor is on text as always (outside tests this will get initialized correctly)Editor_state.cursor1.line = 2-- backspacing deletes the drawingedit.run_after_keychord(Editor_state, 'backspace')check_eq(#Editor_state.lines, 1, 'F - test_backspace_to_delete_drawing/#lines')check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_to_delete_drawing/cursor')
-- primitives for editing drawingsDrawing = {}require 'drawing_tests'-- All drawings span 100% of some conceptual 'page width' and divide it up-- into 256 parts.function Drawing.draw(State, line_index, y)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]line_cache.starty = ylocal pmx,pmy = App.mouse_x(), App.mouse_y()if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) thenApp.color(Icon_color)love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width))if icon[State.current_drawing_mode] thenicon[State.current_drawing_mode](State.right-22, line_cache.starty+4)elseicon[State.previous_drawing_mode](State.right-22, line_cache.starty+4)endif App.mouse_down(1) and love.keyboard.isDown('h') thendraw_help_with_mouse_pressed(State, line_index)returnendendif line.show_help thendraw_help_without_mouse_pressed(State, line_index)returnendlocal mx = Drawing.coord(pmx-State.left, State.width)local my = Drawing.coord(pmy-line_cache.starty, State.width)for _,shape in ipairs(line.shapes) doassert(shape)if geom.on_shape(mx,my, line, shape) thenApp.color(Focus_stroke_color)elseApp.color(Stroke_color)endDrawing.draw_shape(line, shape, line_cache.starty, State.left,State.right)endlocal function px(x) return Drawing.pixels(x, State.width)+State.left endlocal function py(y) return Drawing.pixels(y, State.width)+line_cache.starty endfor i,p in ipairs(line.points) doif p.deleted == nil thenif Drawing.near(p, mx,my, State.width) thenApp.color(Focus_stroke_color)love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance)elseApp.color(Stroke_color)love.graphics.circle('fill', px(p.x),py(p.y), 2)endif p.name then-- TODO: cliplocal x,y = px(p.x)+5, py(p.y)+5love.graphics.print(p.name, x,y)if State.current_drawing_mode == 'name' and i == line.pending.target_point then-- create a faint red box for the nameApp.color(Current_name_background_color)local name_text-- TODO: avoid computing name width on every repaintif p.name == '' thenname_text = State.emelsename_text = App.newText(love.graphics.getFont(), p.name)endlove.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height)endendendendApp.color(Current_stroke_color)Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right)endfunction Drawing.draw_shape(drawing, shape, top, left,right)local width = right-leftlocal function px(x) return Drawing.pixels(x, width)+left endlocal function py(y) return Drawing.pixels(y, width)+top endif shape.mode == 'freehand' thenlocal prev = nilfor _,point in ipairs(shape.points) doif prev thenlove.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y))endprev = pointendelseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal p1 = drawing.points[shape.p1]local p2 = drawing.points[shape.p2]love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y))elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenlocal prev = nilfor _,point in ipairs(shape.vertices) dolocal curr = drawing.points[point]if prev thenlove.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))endprev = currend-- close the looplocal curr = drawing.points[shape.vertices[1]]love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))elseif shape.mode == 'circle' then-- TODO: cliplocal center = drawing.points[shape.center]love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width))elseif shape.mode == 'arc' thenlocal center = drawing.points[shape.center]love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)elseif shape.mode == 'deleted' then-- ignoreelseprint(shape.mode)assert(false)endendfunction Drawing.draw_pending_shape(drawing, top, left,right)local width = right-leftlocal pmx,pmy = App.mouse_x(), App.mouse_y()local function px(x) return Drawing.pixels(x, width)+left endlocal function py(y) return Drawing.pixels(y, width)+top endlocal mx = Drawing.coord(pmx-left, width)local my = Drawing.coord(pmy-top, width)-- recreate pixels from coords to precisely mimic how the drawing will look-- after mouse_releasedpmx,pmy = px(mx), py(my)local shape = drawing.pendingif shape.mode == nil then-- nothing pendingelseif shape.mode == 'freehand' thenlocal shape_copy = deepcopy(shape)Drawing.smoothen(shape_copy)Drawing.draw_shape(drawing, shape_copy, top, left,right)elseif shape.mode == 'line' thenif mx < 0 or mx >= 256 or my < 0 or my >= drawing.h thenreturnendlocal p1 = drawing.points[shape.p1]love.graphics.line(px(p1.x),py(p1.y), pmx,pmy)elseif shape.mode == 'manhattan' thenif mx < 0 or mx >= 256 or my < 0 or my >= drawing.h thenreturnendlocal p1 = drawing.points[shape.p1]if math.abs(mx-p1.x) > math.abs(my-p1.y) thenlove.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y))elselove.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy)endelseif shape.mode == 'polygon' then-- don't close the loop on a pending polygonlocal prev = nilfor _,point in ipairs(shape.vertices) dolocal curr = drawing.points[point]if prev thenlove.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))endprev = currendlove.graphics.line(px(prev.x),py(prev.y), pmx,pmy)elseif shape.mode == 'rectangle' thenlocal first = drawing.points[shape.vertices[1]]if #shape.vertices == 1 thenlove.graphics.line(px(first.x),py(first.y), pmx,pmy)returnendlocal second = drawing.points[shape.vertices[2]]local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))elseif shape.mode == 'square' thenlocal first = drawing.points[shape.vertices[1]]if #shape.vertices == 1 thenlove.graphics.line(px(first.x),py(first.y), pmx,pmy)returnendlocal second = drawing.points[shape.vertices[2]]local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))elseif shape.mode == 'circle' thenlocal center = drawing.points[shape.center]if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h thenreturnendlocal cx,cy = px(center.x), py(center.y)love.graphics.circle('line', cx,cy, geom.dist(cx,cy, App.mouse_x(),App.mouse_y()))elseif shape.mode == 'arc' thenlocal center = drawing.points[shape.center]if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h thenreturnendshape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle)local cx,cy = px(center.x), py(center.y)love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)elseif shape.mode == 'move' then-- nothing pending; changes are immediately committedelseif shape.mode == 'name' then-- nothing pending; changes are immediately committedelseprint(shape.mode)assert(false)endendfunction Drawing.in_drawing(drawing, line_cache, x,y, left,right)if line_cache.starty == nil then return false end -- outside current pagelocal width = right-leftreturn y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < rightendfunction Drawing.mouse_pressed(State, drawing_index, x,y, mouse_button)local drawing = State.lines[drawing_index]local line_cache = State.line_cache[drawing_index]local cx = Drawing.coord(x-State.left, State.width)local cy = Drawing.coord(y-line_cache.starty, State.width)if State.current_drawing_mode == 'freehand' thendrawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}}elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' thenlocal j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)drawing.pending = {mode=State.current_drawing_mode, p1=j}elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' thenlocal j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)drawing.pending = {mode=State.current_drawing_mode, vertices={j}}elseif State.current_drawing_mode == 'circle' thenlocal j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)drawing.pending = {mode=State.current_drawing_mode, center=j}elseif State.current_drawing_mode == 'move' then-- all the action is in mouse_releasedelseif State.current_drawing_mode == 'name' then-- nothingelseprint(State.current_drawing_mode)assert(false)endend-- a couple of operations on drawings need to constantly check the state of the mousefunction Drawing.update(State)if State.lines.current_drawing == nil then return endlocal drawing = State.lines.current_drawinglocal line_cache = State.line_cache[State.lines.current_drawing_index]assert(drawing.mode == 'drawing')local pmx, pmy = App.mouse_x(), App.mouse_y()local mx = Drawing.coord(pmx-State.left, State.width)local my = Drawing.coord(pmy-line_cache.starty, State.width)if App.mouse_down(1) thenif Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) thenif drawing.pending.mode == 'freehand' thentable.insert(drawing.pending.points, {x=mx, y=my})elseif drawing.pending.mode == 'move' thendrawing.pending.target_point.x = mxdrawing.pending.target_point.y = myDrawing.relax_constraints(drawing, drawing.pending.target_point_index)endendelseif State.current_drawing_mode == 'move' thenif Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) thendrawing.pending.target_point.x = mxdrawing.pending.target_point.y = myDrawing.relax_constraints(drawing, drawing.pending.target_point_index)endelse-- do nothingendendfunction Drawing.relax_constraints(drawing, p)for _,shape in ipairs(drawing.shapes) doif shape.mode == 'manhattan' thenif shape.p1 == p thenshape.mode = 'line'elseif shape.p2 == p thenshape.mode = 'line'endelseif shape.mode == 'rectangle' or shape.mode == 'square' thenfor _,v in ipairs(shape.vertices) doif v == p thenshape.mode = 'polygon'endendendendendfunction Drawing.mouse_released(State, x,y, mouse_button)if State.current_drawing_mode == 'move' thenState.current_drawing_mode = State.previous_drawing_modeState.previous_drawing_mode = nilif State.lines.current_drawing thenState.lines.current_drawing.pending = {}State.lines.current_drawing = nilendelseif State.lines.current_drawing thenlocal drawing = State.lines.current_drawinglocal line_cache = State.line_cache[State.lines.current_drawing_index]if drawing.pending thenif drawing.pending.mode == nil then-- nothing pendingelseif drawing.pending.mode == 'freehand' then-- the last point added during update is good enoughDrawing.smoothen(drawing.pending)table.insert(drawing.shapes, drawing.pending)elseif drawing.pending.mode == 'line' thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thendrawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'manhattan' thenlocal p1 = drawing.points[drawing.pending.p1]local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thenif math.abs(mx-p1.x) > math.abs(my-p1.y) thendrawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width)elsedrawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width)endlocal p2 = drawing.points[drawing.pending.p2]App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width))table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'polygon' thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thentable.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width))table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'rectangle' thenassert(#drawing.pending.vertices <= 2)if #drawing.pending.vertices == 2 thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thenlocal first = drawing.points[drawing.pending.vertices[1]]local second = drawing.points[drawing.pending.vertices[2]]local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))table.insert(drawing.shapes, drawing.pending)endelse-- too few points; draw nothingendelseif drawing.pending.mode == 'square' thenassert(#drawing.pending.vertices <= 2)if #drawing.pending.vertices == 2 thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thenlocal first = drawing.points[drawing.pending.vertices[1]]local second = drawing.points[drawing.pending.vertices[2]]local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))table.insert(drawing.shapes, drawing.pending)endendelseif drawing.pending.mode == 'circle' thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thenlocal center = drawing.points[drawing.pending.center]drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'arc' thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h thenlocal center = drawing.points[drawing.pending.center]drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle)table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'name' then-- drop itelseprint(drawing.pending.mode)assert(false)endState.lines.current_drawing.pending = {}State.lines.current_drawing = nilendendendfunction Drawing.keychord_pressed(State, chord)if chord == 'C-p' and not App.mouse_down(1) thenState.current_drawing_mode = 'freehand'elseif App.mouse_down(1) and chord == 'l' thenState.current_drawing_mode = 'line'local _,drawing = Drawing.current_drawing(State)if drawing.pending.mode == 'freehand' thendrawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' thendrawing.pending.p1 = drawing.pending.vertices[1]elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' thendrawing.pending.p1 = drawing.pending.centerenddrawing.pending.mode = 'line'elseif chord == 'C-l' and not App.mouse_down(1) thenState.current_drawing_mode = 'line'elseif App.mouse_down(1) and chord == 'm' thenState.current_drawing_mode = 'manhattan'local drawing = Drawing.select_drawing_at_mouse(State)if drawing.pending.mode == 'freehand' thendrawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)elseif drawing.pending.mode == 'line' then-- do nothingelseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' thendrawing.pending.p1 = drawing.pending.vertices[1]elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' thendrawing.pending.p1 = drawing.pending.centerenddrawing.pending.mode = 'manhattan'elseif chord == 'C-m' and not App.mouse_down(1) thenState.current_drawing_mode = 'manhattan'elseif chord == 'C-g' and not App.mouse_down(1) thenState.current_drawing_mode = 'polygon'elseif App.mouse_down(1) and chord == 'g' thenState.current_drawing_mode = 'polygon'local _,drawing = Drawing.current_drawing(State)if drawing.pending.mode == 'freehand' thendrawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' thenif drawing.pending.vertices == nil thendrawing.pending.vertices = {drawing.pending.p1}endelseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then-- reuse existing verticeselseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' thendrawing.pending.vertices = {drawing.pending.center}enddrawing.pending.mode = 'polygon'elseif chord == 'C-r' and not App.mouse_down(1) thenState.current_drawing_mode = 'rectangle'elseif App.mouse_down(1) and chord == 'r' thenState.current_drawing_mode = 'rectangle'local _,drawing = Drawing.current_drawing(State)if drawing.pending.mode == 'freehand' thendrawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' thenif drawing.pending.vertices == nil thendrawing.pending.vertices = {drawing.pending.p1}endelseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then-- reuse existing (1-2) verticeselseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' thendrawing.pending.vertices = {drawing.pending.center}enddrawing.pending.mode = 'rectangle'elseif chord == 'C-s' and not App.mouse_down(1) thenState.current_drawing_mode = 'square'elseif App.mouse_down(1) and chord == 's' thenState.current_drawing_mode = 'square'local _,drawing = Drawing.current_drawing(State)if drawing.pending.mode == 'freehand' thendrawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' thenif drawing.pending.vertices == nil thendrawing.pending.vertices = {drawing.pending.p1}endelseif drawing.pending.mode == 'polygon' thenwhile #drawing.pending.vertices > 2 dotable.remove(drawing.pending.vertices)endelseif drawing.pending.mode == 'rectangle' then-- reuse existing (1-2) verticeselseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' thendrawing.pending.vertices = {drawing.pending.center}enddrawing.pending.mode = 'square'elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' thenlocal _,drawing,line_cache = Drawing.current_drawing(State)local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)table.insert(drawing.pending.vertices, j)elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') thenlocal _,drawing,line_cache = Drawing.current_drawing(State)local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)while #drawing.pending.vertices >= 2 dotable.remove(drawing.pending.vertices)endtable.insert(drawing.pending.vertices, j)elseif chord == 'C-o' and not App.mouse_down(1) thenState.current_drawing_mode = 'circle'elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' thenlocal _,drawing,line_cache = Drawing.current_drawing(State)drawing.pending.mode = 'arc'local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width)local center = drawing.points[drawing.pending.center]drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my)elseif App.mouse_down(1) and chord == 'o' thenState.current_drawing_mode = 'circle'local _,drawing = Drawing.current_drawing(State)if drawing.pending.mode == 'freehand' thendrawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' thendrawing.pending.center = drawing.pending.p1elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' thendrawing.pending.center = drawing.pending.vertices[1]enddrawing.pending.mode = 'circle'elseif chord == 'C-u' and not App.mouse_down(1) thenlocal drawing_index,drawing,line_cache,i,p = Drawing.select_point_at_mouse(State)if drawing thenif State.previous_drawing_mode == nil thenState.previous_drawing_mode = State.current_drawing_modeendState.current_drawing_mode = 'move'drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i}State.lines.current_drawing_index = drawing_indexState.lines.current_drawing = drawingendelseif chord == 'C-n' and not App.mouse_down(1) thenlocal drawing_index,drawing,line_cache,point_index,p = Drawing.select_point_at_mouse(State)if drawing thenif State.previous_drawing_mode == nil then-- don't clobberState.previous_drawing_mode = State.current_drawing_modeendState.current_drawing_mode = 'name'p.name = ''drawing.pending = {mode=State.current_drawing_mode, target_point=point_index}State.lines.current_drawing_index = drawing_indexState.lines.current_drawing = drawingendelseif chord == 'C-d' and not App.mouse_down(1) thenlocal _,drawing,_,i,p = Drawing.select_point_at_mouse(State)if drawing thenfor _,shape in ipairs(drawing.shapes) doif Drawing.contains_point(shape, i) thenif shape.mode == 'polygon' thenlocal idx = table.find(shape.vertices, i)assert(idx)table.remove(shape.vertices, idx)if #shape.vertices < 3 thenshape.mode = 'deleted'endelseshape.mode = 'deleted'endendenddrawing.points[i].deleted = trueendlocal drawing,_,_,shape = Drawing.select_shape_at_mouse(State)if drawing thenshape.mode = 'deleted'endelseif chord == 'C-h' and not App.mouse_down(1) thenlocal drawing = Drawing.select_drawing_at_mouse(State)if drawing thendrawing.show_help = trueendelseif chord == 'escape' and App.mouse_down(1) thenlocal _,drawing = Drawing.current_drawing(State)drawing.pending = {}endendfunction Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)if firstx == secondx thenreturn x,secondy, x,firstyendif firsty == secondy thenreturn secondx,y, firstx,yendlocal first_slope = (secondy-firsty)/(secondx-firstx)-- slope of second edge:-- -1/first_slope-- equation of line containing the second edge:-- y-secondy = -1/first_slope*(x-secondx)-- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0-- now we want to find the point on this line that's closest to the mouse pointer.-- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equationlocal a = 1/first_slopelocal c = -secondy - secondx/first_slopelocal thirdx = round(((x-a*y) - a*c) / (a*a + 1))local thirdy = round((a*(-x + a*y) - c) / (a*a + 1))-- slope of third edge = first_slope-- equation of line containing third edge:-- y - thirdy = first_slope*(x-thirdx)-- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0-- now we want to find the point on this line that's closest to the first pointlocal a = -first_slopelocal c = -thirdy + thirdx*first_slopelocal fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1))local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1))return thirdx,thirdy, fourthx,fourthyendfunction Drawing.complete_square(firstx,firsty, secondx,secondy, x,y)-- use x,y only to decide which side of the first edge to complete the square onlocal deltax = secondx-firstxlocal deltay = secondy-firstylocal thirdx = secondx+deltaylocal thirdy = secondy-deltaxif not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) thendeltax = -deltaxdeltay = -deltaythirdx = secondx+deltaythirdy = secondy-deltaxendlocal fourthx = firstx+deltaylocal fourthy = firsty-deltaxreturn thirdx,thirdy, fourthx,fourthyendfunction Drawing.current_drawing(State)local x, y = App.mouse_x(), App.mouse_y()for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal line_cache = State.line_cache[drawing_index]if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) thenreturn drawing_index,drawing,line_cacheendendendreturn nilendfunction Drawing.select_shape_at_mouse(State)for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal x, y = App.mouse_x(), App.mouse_y()local line_cache = State.line_cache[drawing_index]if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)for i,shape in ipairs(drawing.shapes) doassert(shape)if geom.on_shape(mx,my, drawing, shape) thenreturn drawing,line_cache,i,shapeendendendendendendfunction Drawing.select_point_at_mouse(State)for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal x, y = App.mouse_x(), App.mouse_y()local line_cache = State.line_cache[drawing_index]if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)for i,point in ipairs(drawing.points) doassert(point)if Drawing.near(point, mx,my, State.width) thenreturn drawing_index,drawing,line_cache,i,pointendendendendendendfunction Drawing.select_drawing_at_mouse(State)for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal x, y = App.mouse_x(), App.mouse_y()local line_cache = State.line_cache[drawing_index]if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) thenreturn drawingendendendendfunction Drawing.contains_point(shape, p)if shape.mode == 'freehand' then-- not supportedelseif shape.mode == 'line' or shape.mode == 'manhattan' thenreturn shape.p1 == p or shape.p2 == pelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenreturn table.find(shape.vertices, p)elseif shape.mode == 'circle' thenreturn shape.center == pelseif shape.mode == 'arc' thenreturn shape.center == p-- ugh, how to support angleselseif shape.mode == 'deleted' then-- already doneelseprint(shape.mode)assert(false)endendfunction Drawing.smoothen(shape)assert(shape.mode == 'freehand')for _=1,7 dofor i=2,#shape.points-1 dolocal a = shape.points[i-1]local b = shape.points[i]local c = shape.points[i+1]b.x = round((a.x + b.x + c.x)/3)b.y = round((a.y + b.y + c.y)/3)endendendfunction round(num)return math.floor(num+.5)endfunction Drawing.find_or_insert_point(points, x,y, width)-- check if UI would snap the two points togetherfor i,point in ipairs(points) doif Drawing.near(point, x,y, width) thenreturn iendendtable.insert(points, {x=x, y=y})return #pointsendfunction Drawing.near(point, x,y, width)local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width)local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width)return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distanceendfunction Drawing.pixels(n, width) -- parts to pixelsreturn math.floor(n*width/256)endfunction Drawing.coord(n, width) -- pixels to partsreturn math.floor(n*256/width)endfunction table.find(h, x)for k,v in pairs(h) doif v == x thenreturn kendendend
-- 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 drawnlocal 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 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 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') end--? 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)--== 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)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 thentable.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.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 endtop2 = 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_top1elseend--? 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 lineendelse-- 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 lineendelseif 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')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) thenendendfunction 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-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.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+1endendfunction Text.right_without_scrollB(State)if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) thenState.cursor1.posB = State.cursor1.posB+1-- overflow back into A sideendendfunction 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)--? 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 endendtop2 = 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 leaksendfunction 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 thenreturn {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}elseif loc2.line == 1 thenreturn 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+$', '')endendfunction starts_with(s, sub)return s:find(sub, 1, --[[no escapes]] true) == 1endfunction ends_with(s, sub)return s:reverse():find(sub:reverse(), 1, --[[no escapes]] true) == 1State.link_cache = {}function source.link_exists(State, filename)if State.link_cache == nil thenState.link_cache = {}endif State.link_cache[filename] == nil thenState.link_cache[filename] = file_exists(filename)endreturn State.link_cache[filename]endif State.lines[loc1.line].mode == 'drawing' thenreturn {line=loc1.line, screen_line=1, screen_pos=1}endif top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' thenlocal h = State.line_heightif y - h < State.top thenbreakendy = y - helseassert(top2.line > 1)assert(State.lines[top2.line-1].mode == 'drawing')-- We currently can't draw partial drawings, so either skip it entirely-- or not at all.local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)if y - h < State.top thenbreakend--? print('skipping drawing of height', h)y = y - hlocal y = State.topwhile State.cursor1.line <= #State.lines doif State.lines[State.cursor1.line].mode == 'text' thenbreakend--? print('cursor skips', State.cursor1.line)y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)State.cursor1.line = State.cursor1.line + 1end-- hack: insert a text line at bottom of file if necessaryif State.cursor1.line > #State.lines thenassert(State.cursor1.line == #State.lines+1)table.insert(State.lines, {mode='text', data=''})table.insert(State.line_cache, {})end--? print(y, App.screen.height, App.screen.height-State.line_height)if y > App.screen.height - State.line_height thenlocal new_cursor_line = State.cursor1.linewhile new_cursor_line <= #State.lines-1 donew_cursor_line = new_cursor_line+1if State.lines[new_cursor_line].mode == 'text' thenState.cursor1 = {line=new_cursor_line, pos=1}breakendendelseelselocal new_cursor_line = State.cursor1.linewhile new_cursor_line <= #State.lines-1 donew_cursor_line = new_cursor_line+1if State.lines[new_cursor_line].mode == 'text' thenState.cursor1 = {line=new_cursor_line, pos=1}breakendendassert(State.lines[State.cursor1.line].mode == 'text')elselocal new_cursor_line = State.cursor1.linewhile new_cursor_line > 1 donew_cursor_line = new_cursor_line-1if State.lines[new_cursor_line].mode == 'text' thenState.cursor1 = {line = new_cursor_line,pos = utf8.len(State.lines[new_cursor_line].data) + 1,}breakendendState.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copylocal new_cursor_line = State.cursor1.linewhile new_cursor_line < #State.lines donew_cursor_line = new_cursor_line+1if State.lines[new_cursor_line].mode == 'text' thenState.cursor1 = {line = new_cursor_line,pos = Text.nearest_cursor_pos(State.lines[new_cursor_line].data, State.cursor_x, State.left),}--? print(State.cursor1.pos)breakendassert(State.lines[State.cursor1.line].mode == 'text')local new_cursor_line = State.cursor1.linewhile new_cursor_line > 1 donew_cursor_line = new_cursor_line-1if State.lines[new_cursor_line].mode == 'text' thenState.cursor1 = {line=State.cursor1.line-1, posB=nil}Text.populate_screen_line_starting_pos(State, State.cursor1.line)local prev_line_cache = State.line_cache[State.cursor1.line]local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1breakendlocal new_cursor_line = State.cursor1.linewhile new_cursor_line > 1 donew_cursor_line = new_cursor_line-1if State.lines[new_cursor_line].mode == 'text' 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) - 1breakendassert(State.lines[State.cursor1.line].mode == 'text')State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB}if State.lines[State.screen_top1.line].mode == 'text' theny = y - State.line_heightelseif State.lines[State.screen_top1.line].mode == 'drawing' theny = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)endtable.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})if State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endState.selection1 = {}State.selection1 = {}if State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endState.selection1 = {}State.selection1 = {}if State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endState.selection1 = {}State.selection1 = {}if State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endState.selection1 = {}State.selection1 = {}if State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB}endState.selection1 = {}State.selection1 = {}if State.lines[State.cursor1.line+1].mode == 'text' 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].dataBendif State.selection1.line thenText.delete_selection(State, State.left, State.right)schedule_save(State)returnendif State.lines[State.cursor1.line-1].mode == 'drawing' thentable.remove(State.lines, State.cursor1.line-1)table.remove(State.line_cache, State.cursor1.line-1)else-- 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)endif State.selection1.line thenText.delete_selection(State, State.left, State.right)schedule_save(State)returnendState.selection1 = {}--? print('chord', chord, State.selection1.line, State.selection1.pos)if line.mode ~= 'text' then return endif line.mode ~= 'text' then return endendif State.selection1.line thenlocal lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)Text.draw_highlight(State, line, x,y, pos, lo,hi)-- Make [[WikiWords]] (single word, all in one screen line) clickable.local trimmed_word = rtrim(frag) -- compute_fragments puts whitespace at the endif starts_with(trimmed_word, '[[') and ends_with(trimmed_word, ']]') thenlocal filename = trimmed_word:gsub('^..(.*)..$', '%1')if source.link_exists(State, filename) thenlocal filename_text = App.newText(love.graphics.getFont(), filename)button(State, 'link', {x=x+App.width(to_text('[[')), y=y, w=App.width(filename_text), h=State.line_height, color={1,1,1},icon = icon.hyperlink_decoration,onpress1 = function()source.switch_to_file(filename)end,})endendendif State.selection1.line thenlocal lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)Text.draw_highlight(State, line, x,y, pos, lo,hi)if not hide_cursor and not State.search_term thenif not hide_cursor and not State.search_term thenfunction Text.draw(State, line_index, y, startpos, startposB, hide_cursor)
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.right, 200-Margin_right, 'F - test_resize_window/right_margin')-- 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')endendfunction test_show_log_browser_side()io.write('\ntest_show_log_browser_side')App.screen.init{width=300, height=300}Display_width = App.screen.widthCurrent_app = 'source'Editor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Text.redraw_all(Editor_state)Log_browser_state = edit.initialize_test_state()Text.redraw_all(Log_browser_state)log_browser.parse(Log_browser_state)check(not Show_log_browser_side, 'F - test_show_log_browser_side/baseline')-- pressing ctrl+l shows log-browser sideApp.wait_fake_time(0.1)App.run_after_keychord('C-l')check(Show_log_browser_side, 'F - test_show_log_browser_side')endfunction test_show_log_browser_side_doubles_window_width_if_possible()io.write('\ntest_show_log_browser_side_doubles_window_width_if_possible')-- initialize screen dimensions to half widthApp.screen.init{width=300, height=300}Display_width = App.screen.width*2-- initialize source app with left side occupying entire window (half the display)Current_app = 'source'Editor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.left = Margin_leftEditor_state.right = App.screen.width - Margin_rightlocal old_editor_right = Editor_state.rightText.redraw_all(Editor_state)Log_browser_state = edit.initialize_test_state()-- log browser has some arbitrary marginsLog_browser_state.left = 200 + Margin_leftLog_browser_state.right = 400Text.redraw_all(Log_browser_state)log_browser.parse(Log_browser_state)-- display log browserApp.wait_fake_time(0.1)App.run_after_keychord('C-l')-- window width is doubledcheck_eq(App.screen.width, 600, 'F - test_show_log_browser_side_doubles_window_width_if_possible/display:width')-- left side margins are unchangedcheck_eq(Editor_state.left, Margin_left, 'F - test_show_log_browser_side_doubles_window_width_if_possible/edit:left')check_eq(Editor_state.right, old_editor_right, 'F - test_show_log_browser_side_doubles_window_width_if_possible/edit:right')-- log browser margins are adjustedcheck_eq(Log_browser_state.left, App.screen.width/2 + Margin_left, 'F - test_show_log_browser_side_doubles_window_width_if_possible/log:left')check_eq(Log_browser_state.right, App.screen.width - Margin_right, 'F - test_show_log_browser_side_doubles_window_width_if_possible/log:right')endfunction test_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width()io.write('\ntest_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width')-- initialize screen dimensions and indicate that it is maximizedApp.screen.init{width=300, height=300}Display_width = 300-- initialize source app with left side occupying more than half the displayCurrent_app = 'source'Editor_state = edit.initialize_test_state()Editor_state.filename = 'foo'Editor_state.left = Margin_leftEditor_state.right = 200Text.redraw_all(Editor_state)Log_browser_state = edit.initialize_test_state()-- log browser has some arbitrary marginsLog_browser_state.left = 200 + Margin_leftLog_browser_state.right = 400Text.redraw_all(Log_browser_state)log_browser.parse(Log_browser_state)-- display log browserApp.wait_fake_time(0.1)App.run_after_keychord('C-l')-- margins are now adjustedcheck_eq(Editor_state.left, Margin_left, 'F - test_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width/edit:left')check_eq(Editor_state.right, App.screen.width/2 - Margin_right, 'F - test_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width/edit:right')check_eq(Log_browser_state.left, App.screen.width/2 + Margin_left, 'F - test_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width/log:left')check_eq(Log_browser_state.right, App.screen.width - Margin_right, 'F - test_show_log_browser_side_resizes_both_sides_if_cannot_double_window_width/log:right')check_eq(Editor_state.width, 200-Margin_left-Margin_right, 'F - test_resize_window/drawing_width')check_eq(Editor_state.left, Margin_left, 'F - test_resize_window/left_margin')-- ugly; resize switches to real, non-test marginscheck_eq(Editor_state.right, 300 - Test_margin_right, 'F - test_resize_window/baseline/right_margin')
-- 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 endelseendendendif #result == 0 thenendreturn 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) doendendoutfile:close()end-- 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 endelseendendif #result == 0 thenendreturn resultendfunction load_drawing_from_array(iter, a, i)local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}local linewhile true doi, line = iter(a, i)assert(i)--? print(i)if line == '```' then break endlocal shape = json.decode(line)if shape.mode == 'freehand' then-- no changes neededelseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal name = shape.p1.nameshape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.p1].name = namename = shape.p2.nameshape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.p2].name = nameelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenfor i,p in ipairs(shape.vertices) dolocal name = p.nameshape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.vertices[i]].name = nameendelseif shape.mode == 'circle' or shape.mode == 'arc' thenlocal name = shape.center.nameshape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.center].name = nameelseif shape.mode == 'deleted' then-- ignoreelseprint(shape.mode)assert(false)endtable.insert(drawing.shapes, shape)endreturn i, drawingendfunction is_absolute_path(path)local os_path_separator = package.config:sub(1,1)if os_path_separator == '/' then-- POSIX systems permit backslashes in filenamesreturn path:sub(1,1) == '/'elseif os_path_separator == '\\' thenlocal f = path:sub(1,1)return f == '/' or f == '\\'elseerror('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')endendfunction is_relative_path(path)return not is_absolute_path(path)endfunction empty(h)for _,_ in pairs(h) doreturn falseendreturn trueendendfunction dirname(path)local os_path_separator = package.config:sub(1,1)if os_path_separator == '/' then-- POSIX systems permit backslashes in filenamesreturn path:match('.*/') or './'elseif os_path_separator == '\\' thenreturn path:match('.*[/\\]') or './'elseerror('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')endendfunction test_dirname()check_eq(dirname('a/b'), 'a/', 'F - test_dirname')check_eq(dirname('x'), './', 'F - test_dirname/current')endfunction basename(path)local os_path_separator = package.config:sub(1,1)if os_path_separator == '/' then-- POSIX systems permit backslashes in filenamesreturn string.gsub(path, ".*/(.*)", "%1")elseif os_path_separator == '\\' thenreturn string.gsub(path, ".*[/\\](.*)", "%1")elseerror('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')endif path:sub(2,2) == ':' then return true end -- DOS drive letter followed by volume separatortable.insert(result, {mode='text', data=''})--? print('inserting text')local line_info = {mode='text'}if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')elseline_info.data = lineendtable.insert(result, line_info)--? print(line)if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated--? print('inserting drawing')i, drawing = load_drawing_from_array(next_line, a, i)--? print('i now', i)table.insert(result, drawing)endfunction load_drawing(infile_next_line)local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}while true dolocal line = infile_next_line()assert(line)if line == '```' then break endlocal shape = json.decode(line)if shape.mode == 'freehand' then-- no changes neededelseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal name = shape.p1.nameshape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.p1].name = namename = shape.p2.nameshape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.p2].name = nameelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenfor i,p in ipairs(shape.vertices) dolocal name = p.nameshape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.vertices[i]].name = nameendelseif shape.mode == 'circle' or shape.mode == 'arc' thenlocal name = shape.center.nameshape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)drawing.points[shape.center].name = nameelseif shape.mode == 'deleted' then-- ignoreelseprint(shape.mode)assert(false)endtable.insert(drawing.shapes, shape)endreturn drawingendfunction store_drawing(outfile, drawing)outfile:write('```lines\n')for _,shape in ipairs(drawing.shapes) doif shape.mode == 'freehand' thenoutfile:write(json.encode(shape), '\n')elseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]})outfile:write(line, '\n')elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenlocal obj = {mode=shape.mode, vertices={}}for _,p in ipairs(shape.vertices) dotable.insert(obj.vertices, drawing.points[p])endlocal line = json.encode(obj)outfile:write(line, '\n')elseif shape.mode == 'circle' thenoutfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\n')elseif shape.mode == 'arc' thenoutfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle}), '\n')elseif shape.mode == 'deleted' then-- ignoreelseprint(shape.mode)assert(false)endendoutfile:write('```\n')if line.mode == 'drawing' thenstore_drawing(outfile, line)elseoutfile:write(line.data)if line.dataB and #line.dataB > 0 thenoutfile:write(Fold)outfile:write(line.dataB)endoutfile:write('\n')table.insert(result, {mode='text', data=''})local line_info = {mode='text'}if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')elseline_info.data = lineendtable.insert(result, line_info)if line == '```lines' then -- inflexible with whitespace since these files are always autogeneratedtable.insert(result, load_drawing(infile_next_line))
-- 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 = {-- 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,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.fixup_cursor(State)for i,line in ipairs(State.lines) doif line.mode == 'text' thenState.cursor1.line = ibreakendendendState.button_handlers = {}App.color(Text_color)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]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)endy = y + State.line_height--? print('=> y', y)elseif line.mode == 'drawing' theny = y+Drawing_padding_topDrawing.draw(State, line_index, y)y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottomelseprint(line.mode)assert(false)endendif 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 endif 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) doendendendfunction 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 textState.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)endif 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)})elsefor _,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,14, -- 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)endApp.screen.width - Test_margin_right,Test_margin_right = 0-- Insulate tests from some key globals so I don't have to change the vast-- majority of tests when they're modified for the real app.-- dispatch to drawing or textelseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then-- DON'T reset line_cache.starty herelocal drawing_index, drawing = Drawing.current_drawing(State)if drawing_index thenlocal before = snapshot(State, drawing_index)Drawing.keychord_pressed(State, chord)record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})schedule_save(State)endelseif chord == 'escape' and not App.mouse_down(1) thenfor _,line in ipairs(State.lines) doif line.mode == 'drawing' thenline.show_help = falseendendelseif State.current_drawing_mode == 'name' thenif chord == 'return' thenState.current_drawing_mode = State.previous_drawing_modeState.previous_drawing_mode = nilelselocal before = snapshot(State, State.lines.current_drawing_index)local drawing = State.lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]if chord == 'escape' thenp.name = nilrecord_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})elseif chord == 'backspace' thenlocal len = utf8.len(p.name)local byte_offset = Text.offset(p.name, len-1)if len == 1 then byte_offset = 0 endp.name = string.sub(p.name, 1, byte_offset)record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})endendschedule_save(State)-- invalidate various cached bits of linesState.lines.current_drawing = nilState.selection1 = deepcopy(src.selection)-- invalidate various cached bits of linesState.lines.current_drawing = nilState.selection1 = deepcopy(src.selection)elseif chord == 'M-d' thenelseif chord == 'M-b' thenif State.selection1.line andnot State.lines.current_drawing and-- printable character created using shift key => delete selection-- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)(not App.shift_down() or utf8.len(key) == 1) andchord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and backspace ~= 'delete' and not App.is_cursor_movement(chord) thenText.delete_selection(State, State.left, State.right)endelseif State.current_drawing_mode == 'name' thenlocal before = snapshot(State, State.lines.current_drawing_index)local drawing = State.lines.current_drawinglocal p = drawing.points[drawing.pending.target_point]p.name = p.name..trecord_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})if State.search_term then return end--? print('release')if State.lines.current_drawing thenDrawing.mouse_released(State, x,y, mouse_button)schedule_save(State)if Drawing.before thenrecord_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})Drawing.before = nilendelsefor line_index,line in ipairs(State.lines) doif line.mode == 'text' thenif Text.in_line(State, line_index, x,y) then--? print('reset selection')local pos,posB = Text.to_pos_on_line(State, line_index, x, y)State.cursor1 = {line=line_index, pos=pos, posB=posB}--? print('cursor', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)if State.mousepress_shift thenif State.old_selection1.line == nil thenState.selection1 = State.old_cursor1elseState.selection1 = State.old_selection1endendState.old_cursor1, State.old_selection1, State.mousepress_shift = nilif eq(State.cursor1, State.selection1) thenState.selection1 = {}endbreakendendend--? print('selection:', State.selection1.line, State.selection1.pos)endif line.mode == 'text' thenif Text.in_line(State, line_index, x,y) then-- delicate dance between cursor, selection and old cursor/selection-- scenarios:-- regular press+release: sets cursor, clears selection-- shift press+release:-- sets selection to old cursor if not set otherwise leaves it untouched-- sets cursor-- press and hold to start a selection: sets selection on press, cursor on release-- press and hold, then press shift: ignore shift-- i.e. mouse_released should never look at shift stateState.old_cursor1 = State.cursor1State.old_selection1 = State.selection1State.mousepress_shift = App.shift_down()local pos,posB = Text.to_pos_on_line(State, line_index, x, y)--? print(x,y, 'setting cursor:', line_index, pos, posB)breakendelseif line.mode == 'drawing' thenlocal line_cache = State.line_cache[line_index]if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) thenState.lines.current_drawing_index = line_indexState.lines.current_drawing = lineDrawing.before = snapshot(State, line_index)Drawing.mouse_pressed(State, line_index, x,y, mouse_button)breakendState.selection1 = {line=line_index, pos=pos, posB=posB}--? print('selection', State.selection1.line, State.selection1.pos, State.selection1.posB)--? print('press')Drawing.update(State, dt)y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB, hide_cursor)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.posBendendif line.data == '' then-- button to insert new drawingbutton(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,0},icon = icon.insert_drawing,onpress1 = function()Drawing.before = snapshot(State, line_index-1, line_index)table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})table.insert(State.line_cache, line_index, {})if State.cursor1.line >= line_index thenState.cursor1.line = State.cursor1.line+1endschedule_save(State)record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)})end,})if line.mode == 'text' then--? print('draw:', y, line_index, line, line.mode)if #State.lines ~= #State.line_cache thenprint(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))assert(false)endfunction edit.draw(State, hide_cursor)filename = love.filesystem.getUserDirectory()..'/lines.txt', -- '/' should work even on Windowscurrent_drawing_mode = 'line',previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming pointsselection1 = {},-- some extra state to compute selection between mouse press and releaseold_cursor1 = nil,old_selection1 = nil,mousepress_shift = nil,-- when selecting text, avoid recomputing some state on every single framerecent_mouse = {},-- a line is either bifold text or a drawing-- a line of bifold text consists of an A side and an optional B side-- mode = 'text',-- string data,-- string dataB,-- expanded: whether to show B side-- a drawing is a table with:-- mode = 'drawing'-- a (y) coord in pixels (updated while painting screen),-- a (h)eight,-- an array of points, and-- an array of shapes-- a shape is a table containing:-- a mode-- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing)-- an array vertices for mode 'polygon', 'rectangle', 'square'-- p1, p2 for mode 'line'-- center, radius for mode 'circle'-- center, radius, start_angle, end_angle for mode 'arc'-- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide-- The field names are carefully chosen so that switching modes in midstream-- remembers previously entered points where that makes sense.lines = {{mode='text', data='', dataB=nil, expanded=nil}}, -- array of linesSame_point_distance = 4 -- pixel distance at which two points are considered the sameDrawing_padding_top = 10Drawing_padding_bottom = 10Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottomIcon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawingsHelp_color = {r=0, g=0.5, b=0}Help_background_color = {r=0, g=0.5, b=0, a=0.1}Stroke_color = {r=0, g=0, b=0}Current_stroke_color = {r=0.7, g=0.7, b=0.7} -- in process of being drawnCurrent_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being editedHyperlink_decoration_color = {r=0.4, g=0.4, b=1}
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 = {'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,}-- 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_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.Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=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 dimensions-- 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)--? 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.filenameendfunction 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 window-- 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)print('initializing source position')if Settings == nil then Settings = {} endif Settings.source == nil then Settings.source = {} endendfunction 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)endfunction source.draw()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')end--? 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,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)--? 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 thenelseApp.screen.width = Editor_state.right + Margin_rightend--? print('setting window:', App.screen.width, App.screen.height)--? print('done setting window')-- try to restore position if possible-- if the window gets wider the window manager may not respect thisreturnendif chord == 'C-g' thenShow_file_navigator = truereturnendif 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]endif not App.run_tests thensource.set_window_position_from_settings(Settings.source)endApp.screen.resize(App.screen.width, App.screen.height, App.screen.flags)App.screen.width = math.min(Display_width, App.screen.width*2)Editor_state.right = App.screen.width/2 - Margin_rightLog_browser_state.left = App.screen.width/2 + Margin_leftLog_browser_state.right = App.screen.width - Margin_rightif Show_file_navigator thentextinput_on_file_navigator(t)returnendif x < Editor_state.right + Margin_right thenif Show_file_navigator and y < Menu_status_bar_height + File_navigation.num_lines * Editor_state.line_height then-- send click to buttonsedit.mouse_pressed(Editor_state, x,y, mouse_button)returnendfilename=Editor_state.filename,cursors=File_navigation.cursors,File_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()source.draw_menu_bar()edit.draw(Editor_state, --[[hide cursor?]] Show_file_navigator)if File_navigation.cursors[filename] thenEditor_state.screen_top1 = File_navigation.cursors[filename].screen_top1Editor_state.cursor1 = File_navigation.cursors[filename].cursor1elseEditor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}end-- save cursor positionFile_navigation.cursors[Editor_state.filename] = {cursor1=Editor_state.cursor1, screen_top1=Editor_state.screen_top1}love.window.setTitle('text.love - source')Settings.source.x, Settings.source.y, Settings.source.displayindex = App.screen.position()App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)App.screen.resize(0, 0) -- maximizeDisplay_width, Display_height, App.screen.flags = App.screen.size()Editor_state.filename = 'run.lua'Editor_state.filename = basename(Editor_state.filename) -- migrate settings that used full paths; we now support only relative paths within the appif settings.cursors thenFile_navigation.cursors = settings.cursorsEditor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1else-- migrate old settingsEditor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}endApp.screen.resize(App.screen.width, App.screen.height, App.screen.flags)App.screen.resize(0, 0) -- maximizeDisplay_width, Display_height, App.screen.flags = App.screen.size()function source.initialize_edit_side()load_from_disk(Editor_state)Text.redraw_all(Editor_state)if File_navigation.cursors[Editor_state.filename] thenEditor_state.screen_top1 = File_navigation.cursors[Editor_state.filename].screen_top1Editor_state.cursor1 = File_navigation.cursors[Editor_state.filename].cursor1elselove.window.setTitle('text.love - source')source.initialize_edit_side()log_new('source')Menu_status_bar_height = 5 + --[[line height in tests]] 15 + 5File_navigation.candidates = File_navigation.all_candidates -- modified with filterfilter = '',cursors = {}, -- filename to cursor1, screen_top1'geom','drawing_tests','drawing','help',all_candidates = {log_render = {}
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 dimensionsApp.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 filename = Editor_state.filenameendreturn {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]endx=Settings.x, y=Settings.y, displayindex=Settings.displayindex,if is_relative_path(filename) thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windowsif Current_app == 'run' thenSettings.x, Settings.y, Settings.displayindex = love.window.getPosition()endif Settings == nil thenSettings = {}endlove.window.setPosition(Settings.x, Settings.y, Settings.displayindex)log_new('run')
-- 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 = 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)endlocal restline.filename, line.line_number, rest = line.data:match('%[string "([^:]*)"%]:([^:]*):%s*(.*)')if line.filename == nil thenline.filename, line.line_number, rest = line.data:match('([^:]*):([^:]*):%s*(.*)')endif rest thenline.data = restend
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 thenelseendadd_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 Menu_cursor + width > App.screen.width - 5 thenreturnendApp.color(Menu_command_color)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()endendendendif cursor_highlight thenendApp.color(Menu_command_color)endfunction keychord_pressed_on_file_navigator(chord, key)if chord == 'escape' thenelseif chord == 'return' thenelseif chord == 'backspace' thenlocal len = utf8.len(File_navigation.filter)local byte_offset = Text.offset(File_navigation.filter, len)File_navigation.filter = string.sub(File_navigation.filter, 1, byte_offset-1)File_navigation.index = 1File_navigation.candidates = source.file_navigator_candidates()elseif 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+1endendendfunction log_render.file_navigator_state(o, x,y, w)-- duplicate structure of source.draw_file_navigatorlocal num_lines = source.num_lines_for_file_navigator(o.files)local h = num_lines * Editor_state.line_heightApp.color(Menu_background_color)love.graphics.rectangle('fill', x,y, w,h)-- compute the x,y,width of the current index (in offsets from top left)local x2,y2 = 0,0local width = 0for i,filename in ipairs(o.files) dolocal filename_text = to_text(filename)width = App.width(filename_text)if x2 + width > App.screen.width - 5 theny2 = y2 + Editor_state.line_heightx2 = 0endif i == o.index thenbreakendx2 = x2 + width + 30end-- figure out how much of the menu to displaylocal menu_xmin = math.max(0, x2-w/2)local menu_xmax = math.min(App.screen.width, x2+w/2)-- now selectively print out entrieslocal x3,y3 = 0,y -- x3 is relative, y3 is absolutelocal width = 0for i,filename in ipairs(o.files) dolocal filename_text = to_text(filename)width = App.width(filename_text)if x3 + width > App.screen.width - 5 theny3 = y3 + Editor_state.line_heightx3 = 0endif i == o.index thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', x + x3-menu_xmin - 5, y3-2, width+5*2, Editor_state.line_height+2*2)endif x3 >= menu_xmin and x3 + width < menu_xmax thenApp.color(Menu_command_color)App.screen.draw(filename_text, x + x3-menu_xmin, y3)endx3 = x3 + width + 30end--return h+20endfunction file_navigator_up()local y, x, width = file_coord(File_navigation.index)local index = file_index(y-Editor_state.line_height, x, width)if index thenFile_navigation.index = indexendendfunction file_navigator_down()local y, x, width = file_coord(File_navigation.index)local index = file_index(y+Editor_state.line_height, x, width)if index thenFile_navigation.index = indexendendfunction file_coord(index)local y,x = Menu_status_bar_height, 5for i,filename in ipairs(File_navigation.candidates) dolocal width = App.width(to_text(filename))if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5endif i == index thenreturn y, x, widthendx = x + width + 30endendfunction file_index(fy, fx, fwidth)log_start('file index')log(2, ('for %d %d %d'):format(fy, fx, fwidth))local y,x = Menu_status_bar_height, 5local best_guess, best_guess_x, best_guess_widthfor i,filename in ipairs(File_navigation.candidates) dolocal width = App.width(to_text(filename))if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5endif y == fy thenlog(2, ('%d: correct row; considering %d %s %d %d'):format(y, i, filename, x, width))if best_guess == nil thenlog(2, 'nil')best_guess = ibest_guess_x = xbest_guess_width = widthelseif math.abs(fx + fwidth/2 - x - width/2) < math.abs(fx + fwidth/2 - best_guess_x - best_guess_width/2) thenbest_guess = ibest_guess_x = xbest_guess_width = widthendlog(2, ('best guess now %d %s %d %d'):format(best_guess, File_navigation.candidates[best_guess], best_guess_x, best_guess_width))endx = x + width + 30endlog_end('file index')return best_guessendfunction textinput_on_file_navigator(t)File_navigation.filter = File_navigation.filter..tFile_navigation.candidates = source.file_navigator_candidates()endelseif chord == 'down' thenfile_navigator_down()elseif chord == 'up' thenfile_navigator_up()navigate_to_file(File_navigation.candidates[File_navigation.index])reset_file_navigator()log(2, 'file navigator: '..chord)log(2, {name='file_navigator_state', files=File_navigation.candidates, index=File_navigation.index})function navigate_to_file(s)move_candidate_to_front(s)local candidate = guess_source(s..'.lua')source.switch_to_file(candidate)reset_file_navigator()endfunction move_candidate_to_front(s)local index = array.find(File_navigation.all_candidates, s)assert(index)table.remove(File_navigation.all_candidates, index)table.insert(File_navigation.all_candidates, 1, s)endfunction reset_file_navigator()Show_file_navigator = falseFile_navigation.index = 1File_navigation.filter = ''File_navigation.candidates = File_navigation.all_candidatesendApp.screen.draw(s_text, x,y)x = x + width + 30return x,ybutton(Editor_state, 'menu', {x=x-5, y=y-2, w=width+5*2, h=Editor_state.line_height+2*2, color=colortable(color),onpress1 = function()end})navigate_to_file(s)color = Menu_highlight_colorlocal color = Menu_background_colorendfunction source.file_navigator_candidates()if File_navigation.filter == '' thenreturn File_navigation.all_candidatesendlocal result = {}for _,filename in ipairs(File_navigation.all_candidates) doif starts_with(filename, File_navigation.filter) thentable.insert(result, filename)endendreturn resultendfunction source.num_lines_for_file_navigator(candidates)local result = 1local x = 5for i,filename in ipairs(candidates) dolocal width = App.width(to_text(filename))if x + width > App.screen.width - 5 thenresult = result+1x = 5 + widthelsex = x + width + 30endendreturn resultendfunction add_file_to_menu(x,y, s, cursor_highlight)local s_text = to_text(s)local width = App.width(s_text)if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5function draw_cursor(x, y)-- blink every 0.5sif math.floor(Cursor_time*2)%2 == 0 thenApp.color(Cursor_color)love.graphics.rectangle('fill', x,y, 3,Editor_state.line_height)x,y = add_file_to_menu(x,y, filename, i == File_navigation.index)if Menu_cursor >= App.screen.width - 5 thenbreakendApp.color(Menu_command_color)local filter_text = to_text(File_navigation.filter)App.screen.draw(filter_text, 5, 5)draw_cursor(5 + App.width(filter_text), 5)if File_navigation.num_lines == nil thenFile_navigation.num_lines = source.num_lines_for_file_navigator(File_navigation.candidates)endApp.color(Menu_background_color)love.graphics.rectangle('fill', 0,Menu_status_bar_height, App.screen.width, File_navigation.num_lines * Editor_state.line_height + --[[highlight padding]] 2)local x,y = 5, Menu_status_bar_heightfor i,filename in ipairs(File_navigation.candidates) doApp.screen.draw(s_text, Menu_cursor,5)local s_text = to_text(s)local width = App.width(s_text)add_hotkey_to_menu('alt+d: create/edit debug print')add_hotkey_to_menu('alt+b: expand debug prints')add_hotkey_to_menu('alt+b: collapse debug prints')
-- 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
-- but some files we want to only load sometimesfunction App.load()log_new('session')if love.filesystem.getInfo('config') thenSettings = json.decode(love.filesystem.read('config'))Current_app = Settings.current_append
-- a few text objects we can avoid recomputing unless the font changesText_cache = {}
if Current_app == nil thenCurrent_app = 'run'end
-- blinking cursorCursor_time = 0
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')elseif Current_app == 'source' thenload_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('source_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('icons.lua')load_file_from_source_or_save_directory('drawing.lua')load_file_from_source_or_save_directory('geom.lua')load_file_from_source_or_save_directory('help.lua')load_file_from_source_or_save_directory('drawing_tests.lua')load_file_from_source_or_save_directory('source_tests.lua')elseassert(false, 'unknown app "'..Current_app..'"')endendfunction App.initialize_globals()if Current_app == 'run' thenrun.initialize_globals()elseif Current_app == 'source' thensource.initialize_globals()elseassert(false, 'unknown app "'..Current_app..'"')end
if Current_app == 'run' thenrun.initialize(arg)elseif Current_app == 'source' thensource.initialize(arg)
love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)love.graphics.setBackgroundColor(1,1,1)if love.filesystem.getInfo('config') thenload_settings()
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) end
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}
function App.filedropped(file)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..'"')
if #arg > 1 thenprint('ignoring commandline args after '..arg[1])
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..'"')
endfunction 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.cursorendfunction 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 = emendfunction 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 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)Last_resize_time = App.getTime()endfunction App.filedropped(file)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)
--if Current_app == 'run' thenrun.update(dt)elseif Current_app == 'source' thensource.update(dt)elseassert(false, 'unknown app "'..Current_app..'"')
Cursor_time = Cursor_time + dtedit.update(Editor_state, dt)endfunction 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 Windowsendlocal 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..'"')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.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.textinput(Editor_state, t)
end--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..'"')endendfunction 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..'"')
endfunction 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..'"')end
Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.key_released(Editor_state, key, scancode)
function 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..'"')
-- use this sparinglyfunction to_text(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)
if type(elem) == 'function' thenfor i,x in ipairs(arr) doif elem(x) thenreturn iendendelsefor i,x in ipairs(arr) doif x == elem thenreturn iend
for i,x in ipairs(arr) doif x == elem thenreturn i
function is_absolute_path(path)local os_path_separator = package.config:sub(1,1)if os_path_separator == '/' then-- POSIX systems permit backslashes in filenamesreturn path:sub(1,1) == '/'elseif os_path_separator == '\\' thenif path:sub(2,2) == ':' then return true end -- DOS drive letter followed by volume separatorlocal f = path:sub(1,1)return f == '/' or f == '\\'elseerror('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')endendfunction is_relative_path(path)return not is_absolute_path(path)end
State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos},screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},}
State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos}, screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos}}
-- 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()end
end-- operations on the LÖVE window within the monitor/displayfunction App.screen.resize(width, height, flags)App.screen.width = widthApp.screen.height = heightApp.screen.flags = flagsendfunction App.screen.size()return App.screen.width, App.screen.height, App.screen.flagsendfunction App.screen.move(x,y, displayindex)App.screen.x = xApp.screen.y = yApp.screen.displayindex = displayindex
if Current_app == nil or Current_app == 'run' thenApp.open_for_reading = function(filename) return io.open(filename, 'r') endApp.open_for_writing = function(filename) return io.open(filename, 'w') endelseif Current_app == 'source' then-- HACK: source editor requires a couple of different foundational definitionsApp.open_for_reading =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('r')if ok thenreturn resultelsereturn ok, errendendApp.open_for_writing =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, errendendend
App.open_for_reading = function(filename) return io.open(filename, 'r') endApp.open_for_writing = function(filename) return io.open(filename, 'w') end
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